chore: merge origin/main

This commit is contained in:
Abdessamad Derraz committed 2026-10-27 09:11:43 +01:00
commit 9c34e8891f
22 files changed
+1445 -403

No files matched your search

+3 -1
View File
@@ -193,8 +193,10 @@ jobs:
fi
python scripts/profile_sync.py --added --base "$base" --strict
# ubuntu-latest moved to 26.04, where setup-python has no 3.8 build: the
# job is held on the last image that still carries the floor interpreter.
run-tests:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "2e88f22d2f3cd933d9b198b841367d6e2d7084de228fa0b12dc2f11c71c0c1e9"
$defaultInstallSha256 = "89451086ee0a434877b56d78734aed95be74e62ec5c1a5ab4e54769902ed8f44"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+687 -263
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="2e88f22d2f3cd933d9b198b841367d6e2d7084de228fa0b12dc2f11c71c0c1e9"
DEFAULT_INSTALL_SHA256="89451086ee0a434877b56d78734aed95be74e62ec5c1a5ab4e54769902ed8f44"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 692187,
"sha256": "2e88f22d2f3cd933d9b198b841367d6e2d7084de228fa0b12dc2f11c71c0c1e9",
"size": 715303,
"sha256": "89451086ee0a434877b56d78734aed95be74e62ec5c1a5ab4e54769902ed8f44",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 744736,
"sha256": "17daef144acc01204912de201bbb6bbf8996d1a866a37074e7cbaffe879e911b"
"size": 767852,
"sha256": "07caac3bd999de7eea34292d8ecc806100f6e950a51805d7db3df37af141bc94"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 692187,
"sha256": "2e88f22d2f3cd933d9b198b841367d6e2d7084de228fa0b12dc2f11c71c0c1e9",
"size": 715303,
"sha256": "89451086ee0a434877b56d78734aed95be74e62ec5c1a5ab4e54769902ed8f44",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 692187,
"sha256": "2e88f22d2f3cd933d9b198b841367d6e2d7084de228fa0b12dc2f11c71c0c1e9",
"size": 715303,
"sha256": "89451086ee0a434877b56d78734aed95be74e62ec5c1a5ab4e54769902ed8f44",
"executable": false
},
{
+14 -6
View File
@@ -154,8 +154,8 @@ handheld_guide:
- name: muOS
folder: '`MUOS/bios` on the card'
folder_source: https://github.com/MustardOS/internal/blob/main/share/web/tools/bios-checker.html
devices: [RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX H, RG40XX H, RG40XX V,
RG28XX H, RGCUBEXX, TrimUI Brick, TrimUI Smart Pro]
devices: [RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX, RG40XX H, RG40XX V,
RG28XX, RG CubeXX, TrimUI Brick, TrimUI Smart Pro]
devices_source: https://muos.dev/devices
- name: Onion
folder: '`BIOS` at the root of the card'
@@ -166,7 +166,7 @@ handheld_guide:
folder: '`BIOS` at the root of the card'
folder_source: https://github.com/spruceUI/spruceOS/wiki/10.-Adding-BIOS
devices: [Miyoo A30, Miyoo Flip, Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip,
TrimUI Smart Pro, TrimUI Brick, RG35XX Plus, RG40XX H, Powkiddy RGB30]
TrimUI Smart Pro, TrimUI Brick, RG35XX Plus, RG40XX H, RGB30]
devices_source: https://github.com/spruceUI/spruceOS/wiki/Supported-Devices
- name: Allium
folder: '`BIOS` at the root of the card'
@@ -174,12 +174,14 @@ handheld_guide:
devices: [Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip]
devices_source: https://github.com/goweiwen/Allium
- name: Anbernic stock firmware
devices: [RG35XX Plus, RG35XX H, RG35XX SP, RG28XX]
devices_source: https://github.com/GarlicOS/bootloader_anbernic_rg35xxplus/blob/45b489b41dc3dd1738b14486cda0f2dce66e714f/README.md
- name: ArchR
folder: '`/storage/roms/bios`, on a part only Linux can open'
folder_source: https://arch-r.io/docs/roms/
linux_only: true
devices: [R36S, R33S, R36S Soysauce, Odroid Go Advance, Odroid Go Super, RG351V,
RG351M, GameForce Chi, MagicX XU10, Powkiddy RGB10, Powkiddy RGB10X, K36]
RG351M, GameForce Chi, MagicX XU10, RGB10, RGB10X, K36]
devices_source: https://github.com/archr-linux/Arch-R
- name: CrossMix-OS
folder: '`BIOS` at the root of the card'
@@ -194,10 +196,16 @@ handheld_guide:
- name: JELOS
folder: '`roms/bios` on the games card'
folder_source: https://github.com/JustEnoughLinuxOS/distribution/blob/7b2e116ce6996d56e642a0e2a07288c926db926b/packages/emulators/standalone/retroarch/sources/RK3566/retroarch.cfg#L716
devices: [RG351M, RG351V, RG353M, RG353P, RG353V, RG353VS, RG503, RG552, R33S, R36S,
GameForce Ace, GameForce Chi, Odroid Go Advance, Odroid Go Super, Odroid Go Ultra,
MagicX XU10, RGB10, RGB10 Max 3, RGB10 Max 3 Pro, RGB20S, RGB30, RK2023, X55,
AYANEO Air, AYANEO Air Plus, AYANEO Air Pro, AYANEO 2, AYANEO 2S, AYANEO 2021,
AYANEO Geek, AYANEO Next, AYN Loki Max, AYN Loki Mini Pro, AYN Loki Zero]
devices_source: https://github.com/JustEnoughLinuxOS/distribution/tree/7b2e116ce6996d56e642a0e2a07288c926db926b/packages/hardware/quirks/devices
- name: KOLOS
folder: '`roms/bios` on the games card'
folder_source: https://github.com/AnonLinuxOS/KOLOS/blob/7daf5950ae79bb8775f129468164f730dbc32298/packages/emulators/standalone/retroarch/sources/RK3566/retroarch.cfg#L716
devices: [RG353P, RG353M, RG353V, RG353VS, RG503, Powkiddy RK2023, RGB30, RGB10 Max 3]
devices: [RG353P, RG353M, RG353V, RG353VS, RG503, RK2023, RGB30, RGB10 Max 3]
devices_source: https://github.com/AnonLinuxOS/KOLOS
- name: Koriki
folder: '`BIOS` at the root of the card'
@@ -222,7 +230,7 @@ handheld_guide:
- name: RetroOZ
folder: '`/roms/bios`, as the device names it'
folder_source: https://github.com/southoz/RetroOZ/wiki/Home/4ce4fafb78d647d422e9e468d5bd9cbc63dbe3ea
devices: [OGS, OGA, RGB10Max, RGB10Max2]
devices: [Odroid Go Super, Odroid Go Advance, RGB10 Max, RGB10 Max 2]
devices_source: https://github.com/southoz/RetroOZ/wiki
- name: UnofficialOS
folder: '`roms/bios` on the games card'
+2 -2
View File
@@ -160,7 +160,7 @@ def seal(slim: dict, sidecar: dict, rid: str | None = None) -> tuple[dict, bytes
"""The slim record with its data reference and id, and the sidecar bytes.
``rid`` keeps an id given before (a record carried over from the first
layout); otherwise the id is computed.
layout), otherwise the id is computed.
"""
blob = sidecar_bytes(sidecar)
sealed = {**slim, "data": {"sha256": sha256_hex(blob), "bytes": len(blob)}}
@@ -231,7 +231,7 @@ def parse_lines(text: str, label: str) -> list[dict]:
def appended_only(before: str, after: str) -> str | None:
"""None when ``after`` keeps every line of ``before``, in place, and
only adds lines at its end; otherwise the first line that moved."""
only adds lines at its end, otherwise the first line that moved."""
old, new = before.splitlines(), after.splitlines()
for number, line in enumerate(old, 1):
if number > len(new):
+10 -10
View File
@@ -15,26 +15,26 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, Odroid Go Super, RG351V, +6
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, R35S, RGB10
CrossMix-OS, SD card TrimUI Smart Pro
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
EmuELEC, SD card Odroid Go
JELOS, SD card RetroArch files
KNULLI, SD card Anbernic RG-Arc-S, RG28XX, RG34XX, RG35XX Original, RG35XX Plus, RG35XX H, +16
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, Powkiddy RK2023, RGB30, RGB10 Max 3
JELOS, SD card RG351M, RG351V, RG353M, RG353P, RG353V, RG353VS, RG503, RG552, R33S, R36S, +24
KNULLI, SD card RG ARC-S, RG28XX, RG34XX, RG35XX, RG35XX Plus, RG35XX H, RG35XX 2024, +15
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, RK2023, RGB30, RGB10 Max 3
Koriki, SD card Miyoo Mini
MinUI, SD card Anbernic, Miyoo, TrimUI
MinUI, SD card RG28XX, RG34XX, RG34XX SP, RG35XX, RG35XX Plus, RG35XX H, RG35XX SP, +16
MiyooCFW, SD card BittBoy, Pocket Go, PowKiddy Q90, PowKiddy V90, PowKiddy Q20 Mini, Sup M3, +1
muOS, SD card RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX H, RG40XX H, RG40XX V, +4
MyMinUI, SD card Anbernic, Miyoo
muOS, SD card RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX, RG40XX H, RG40XX V, +4
MyMinUI, SD card RG35XX, Miyoo Mini, Miyoo Mini Flip, Miyoo Mini Plus, Miyoo A30, +16
NextUI, SD card TrimUI Brick, TrimUI Smart Pro
Onion, SD card Miyoo Mini, Miyoo Mini Plus
plumOS, SD card MagicX XU20 V32
RetroOZ, SD card OGS, OGA, RGB10Max, RGB10Max2
ROCKNIX, SD card Anbernic RG351, RG353, RG503, RG552, RG 55G1, RG ARC, RG35XX 2024, +38
RetroOZ, SD card Odroid Go Super, Odroid Go Advance, RGB10 Max, RGB10 Max 2
ROCKNIX, SD card RG351, RG353, RG503, RG552, RG 55G1, RG ARC, RG35XX 2024, RG35XX Plus, +37
spruceOS, SD card Miyoo A30, Miyoo Flip, Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip, +5
UnofficialOS, SD card R35S, R36S, RG351M, RG351P, RG351V, RG353M, RG353P, RG353V, RG503, +5
A/Enter Choose B/Esc Back / Filter ? Help
+2 -2
View File
@@ -13,8 +13,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+2 -2
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
Start Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
Start Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
Start Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+18 -3
View File
@@ -1829,10 +1829,25 @@ class TestManualPlatformPrompt(unittest.TestCase):
self.assertEqual(chosen, [("batocera", Path("~/my bios").expanduser())])
def test_default_directory_comes_from_the_table(self):
answers = iter(["batocera", ""])
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)):
answers = iter(["lakka", ""])
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)), \
unittest.mock.patch.object(install, "_mounted_cards", return_value=[]), \
contextlib.redirect_stdout(io.StringIO()):
chosen = install._prompt_manual_platform("linux")
self.assertEqual(chosen, [("batocera", install.DEFAULT_DESTS["batocera"])])
self.assertEqual(chosen, [("lakka", install.DEFAULT_DESTS["lakka"])])
def test_batocera_on_a_computer_gets_no_default(self):
"""tui_audit_8 P2-4: /userdata/bios, a folder only the Batocera
machine has, was offered on a computer, and Enter took it."""
answers = iter(["batocera", "", "~/share/bios"])
out = io.StringIO()
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)), \
unittest.mock.patch.object(install, "_mounted_cards", return_value=[]), \
contextlib.redirect_stdout(out):
chosen = install._prompt_manual_platform("linux")
self.assertEqual(chosen, [("batocera", Path("~/share/bios").expanduser())])
self.assertIn("SHARE drive", out.getvalue())
self.assertNotIn("/userdata/bios", out.getvalue())
def test_quit_stops_like_ctrl_c(self):
"""`install.sh && retroarch` went on to start RetroArch with no BIOS
+2 -1
View File
@@ -370,7 +370,8 @@ class TheQuestion(unittest.TestCase):
def test_a_console_without_python_says_so_once(self):
asked, put = self.run_finish("y", system="Onion")
self.assertEqual((asked, put), ([], []))
self.assertIn(install.NO_PYTHON_WORDS, self.output)
self.assertIn(install.no_python_words("Onion"), self.output)
self.assertNotIn("This console", self.output)
class TheOption(unittest.TestCase):
+78 -2
View File
@@ -662,7 +662,9 @@ class OfflineMakesNoRequest(Scratch):
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual((self.dest / "scph5501.bin").read_bytes(), US)
self.assertFalse((self.dest / "gba_bios.bin").exists())
self.assertIn("no network request", proc.stdout + proc.stderr)
# Missing, never failed (code_audit_15 P3-1): no FAILED line per file.
self.assertIn("3 not fetched (--offline)", proc.stdout + proc.stderr)
self.assertNotIn("FAILED", proc.stdout + proc.stderr)
self.assertIn("without --offline", proc.stdout + proc.stderr)
self.assertNotIn("trying again", proc.stderr)
@@ -839,7 +841,11 @@ class OfflineLeavesFilesMissing(Scratch):
self.list.write_text(json.dumps(listed))
self.complete()
proc = self.run_installer("--offline", "--manifest", str(self.list), "--no-input")
self.assertIn("bluemsx: could not read", proc.stderr)
# Its files are missing, as the BIOS files --offline leaves out are,
# with or without questions.
self.assertIn("bluemsx: 0 already up to date, 1 need the internet (--offline)",
proc.stdout)
self.assertNotIn("could not read", proc.stderr)
self.assertNotIn("no longer in the source", proc.stdout)
@@ -936,5 +942,75 @@ class OneOfflineVocabulary(Scratch):
self.assertNotIn("install everything", text)
class AuditFifteenOffline(Scratch):
"""code_audit_15 P3-1, P3-5 and tui_audit_8 P3-11: what --offline and a
pack leave out, and what a folder copied from holds, said as such."""
def test_yes_offline_counts_the_rest_missing_never_failed(self):
"""The screen's own install (--yes): each file left out was
"downloading", then FAILED, and pending and failed at once."""
old = self.root / "old"
old.mkdir()
(old / "scph5501.bin").write_bytes(US)
proc = self.run_installer("--offline", "--manifest", str(self.list), "--from",
str(old), "--yes", "--events")
self.assertEqual(proc.returncode, 1, proc.stderr)
events = [json.loads(line) for line in proc.stdout.splitlines()]
summary = next(event for event in events if event["event"] == "summary")
self.assertEqual((summary["installed"], summary["pending"], summary["failed"]),
(1, 3, []))
self.assertNotIn("FAILED", proc.stderr)
self.assertNotIn("downloading", proc.stderr)
self.assertIn("1 file installed, 3 not fetched (--offline)", proc.stderr)
download = next(event for event in events if event["event"] == "download")
self.assertEqual(download["files"], 1)
# The screen reads that summary as missing files, not failures.
state = install.TuiState()
state.summary = summary
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
title, lines = install.tui_outcome(state)
self.assertEqual(title, "Finished with files missing")
self.assertIn("1 installed, 3 not fetched (--offline).", lines[0])
def test_a_pack_is_copied_not_downloaded(self):
downloads = self.root / "downloads"
downloads.mkdir()
pack = _pack(downloads, "RetroArch_v1_BIOS_Pack.zip", PAYLOAD)
proc = self.run_installer("--from-pack", str(pack), "--dry-run", "--no-input")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("Would copy 4 files", proc.stdout)
self.assertIn("from the pack to", proc.stdout)
self.assertNotIn("download", proc.stdout.lower())
def test_a_folder_holding_none_of_the_files_is_said_and_left(self):
empty = self.root / "empty"
(empty / "photos").mkdir(parents=True)
(empty / "photos" / "a.jpg").write_bytes(b"jpeg")
install.LOCAL.reset()
self.addCleanup(install.LOCAL.reset)
files = manifest()["files"]
answers = iter(["f", f"{self.root}/x/../empty", "q"])
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
unittest.mock.patch.object(install, "_ask", lambda _prompt: next(answers)), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._prompt_custom_selection("retroarch", files, [], bios_path=self.dest)
text = out.getvalue()
self.assertIn("empty holds none of these files.", text)
self.assertNotIn("install what is found", text)
self.assertNotIn("..", text)
self.assertEqual(install.LOCAL.folders, [])
# One that holds some is counted.
(empty / "scph5501.bin").write_bytes(US)
answers = iter(["f", str(empty), "q"])
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
unittest.mock.patch.object(install, "_ask", lambda _prompt: next(answers)), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._prompt_custom_selection("retroarch", files, [], bios_path=self.dest)
self.assertIn("1 of 4 found in empty.", out.getvalue())
self.assertIn("Enter) install what is found in", out.getvalue())
if __name__ == "__main__":
unittest.main()
+3 -1
View File
@@ -704,7 +704,9 @@ class TheOneLinerNarrows(unittest.TestCase):
env = dict(repo.env(), HOME=str(home), USERPROFILE=str(home),
XDG_CONFIG_HOME=str(home), RETROBIOS_OS="linux")
command = f"{shlex.quote(sys.executable)} {shlex.quote(str(REPO_ROOT / 'install.py'))}"
for answer, code in (("n", 2), ("y", 0)):
# No to the folders asks the folder again, as the screen's
# Choose another folder does: the input then ends, Ctrl+D.
for answer, code in (("n", 130), ("y", 0)):
with self.subTest(answer=answer):
proc = subprocess.run(
["script", "-qec", command, "/dev/null"],
+517 -13
View File
@@ -2193,7 +2193,10 @@ class AuditRoundThree(unittest.TestCase):
self.assertEqual(proc.returncode, 1)
self.assertIn(f"{root} is not a muOS SD card: no muOS files on it. muOS keeps "
"MUOS at the root of its card.", proc.stderr)
self.assertIn("--dest <card>/MUOS/bios", proc.stderr)
# muOS always names its card: <card>/MUOS/bios would be refused the
# same (code_audit_15 P3-4), the card's top folder is the remedy.
self.assertIn("Name the top folder of the muOS card with --dest", proc.stderr)
self.assertNotIn("<card>/", proc.stderr)
self.assertEqual(list(root.iterdir()), [root / "roms"])
# Its BIOS folder named outright is its card's, checked as the card
# is: on a folder with no MUOS it is refused, as typed it was.
@@ -2861,7 +2864,8 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest("onion", "retroarch", interactive=False)
self.assertEqual(dest, onion / "BIOS")
self.assertIn(f"Onion card found at {onion}", out.getvalue())
self.assertIn(f"Onion card found at {onion.name}, BIOS folder {onion / 'BIOS'}",
out.getvalue())
def test_at_a_terminal_the_card_root_is_asked(self):
onion = self.card(self.ONION)
@@ -3124,15 +3128,17 @@ class TheSetupFilterKnowsDevices(unittest.TestCase):
TYPED = {
"steam deck": {"retroarch", "batocera", "emudeck", "retrodeck"},
"r36s": {"retroarch", "rocknix"},
"rg35xx": {"retroarch", "rocknix", "knulli"},
# MinUI and MyMinUI list their devices (code_audit_15 P2-1), and on
# a computer an Android device is no RetroArch of this machine's.
"r36s": {"retroarch", "rocknix", "myminui"},
"rg35xx": {"retroarch", "rocknix", "knulli", "minui", "myminui"},
"trimui": {"retroarch", "knulli", "minui", "nextui"},
"retroid": {"retroarch", "rocknix", "knulli"},
"retroid": {"rocknix", "knulli"},
"miyoo": {"retroarch", "knulli", "minui", "myminui"},
"anbernic": {"retroarch", "rocknix", "amberelec", "knulli", "minui", "myminui"},
"odin": {"retroarch", "rocknix"},
"odin": {"rocknix"},
"ayn": {"retroarch", "rocknix"},
"rp5": {"retroarch", "rocknix", "knulli"},
"rp5": {"rocknix", "knulli"},
"rog ally": {"retroarch", "emudeck", "retrobat", "bizhawk"},
"onion": {"retroarch"},
}
@@ -4387,7 +4393,7 @@ class AppOnTheCard(unittest.TestCase):
self.assertEqual(install.tui_rows(offline), [])
os.environ.pop(install.OFFLINE_VARIABLE)
_title, lines = install.tui_outcome(cases["no launcher"])
self.assertIn(install.NO_PYTHON_WORDS, lines)
self.assertIn(install.no_python_words("Onion"), lines)
def test_put_by_the_install_itself_no_second_offer(self):
root, bios = _app_card(self, "muOS")
@@ -4951,7 +4957,10 @@ class ThePlanNumbersAddUp(unittest.TestCase):
state.found = [found("scph5500.bin", str(SYSTEM_DIR), "one folder too deep")]
screen = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
self.assertIn("3 files to download", screen)
self.assertIn("1 in place, 3 missing, 1 wrong copy 4 to install: 1 copied from this "
# The backup line under the count it explains, then the split
# (tui_audit_8 P3-5).
self.assertIn("1 in place, 3 missing, 1 wrong copy The 1 wrong copy is replaced, the "
"old one kept in a backup folder. 4 to install: 1 copied from this "
"machine, 3 downloaded", screen)
@@ -5390,7 +5399,7 @@ class AuditSix(unittest.TestCase):
self.assertEqual(args.dest, Path("/mnt/d"))
card = Path("/mnt/d")
with mock.patch.object(install, "_card_what", return_value="Onion card"):
self.assertEqual(install.card_line(card), "D: (Onion card, /mnt/d)")
self.assertEqual(install.card_line(card), "D: (Onion card)")
self.assertEqual(install._typed_folder("D:"), Path("D:"))
def test_a_dest_refused_on_the_setup_list_names_dest(self):
@@ -5407,8 +5416,10 @@ class AuditSix(unittest.TestCase):
self.assertEqual(install.tui_rows(state)[1].label, "Onion, SD card")
self.assertEqual(press(state, "enter"), [])
self.assertEqual(state.screen, "platforms")
self.assertIn("--dest names it: choose another setup.", state.status)
self.assertNotIn("Choose the card's top folder", state.status)
# --dest is fixed: a stick that is no card is named again in the
# command, another setup would not help (code_audit_15 P3-3).
self.assertIn("Run it again with the card's top folder.", state.status)
self.assertNotIn("choose another setup", state.status)
self.assertEqual((state.platform, state.card_bound), ("", None))
def test_on_the_device_a_folder_typed_is_taken_as_typed(self):
@@ -5780,7 +5791,7 @@ class AuditSevenOneCardReading(unittest.TestCase):
text = out.getvalue()
self.assertIn("Type its number or its top folder:", text)
self.assertNotIn("Mount the card", text)
self.assertIn(f"ONION2: Onion files go to {cards[1] / 'BIOS'}", text)
self.assertIn(f"Onion card ONION2, BIOS folder {cards[1] / 'BIOS'}", text)
self.assertEqual(install.detected_os("retroarch", dest), "Onion")
with mock.patch.object(install, "_ask", return_value=""), \
contextlib.redirect_stdout(io.StringIO()) as out:
@@ -6043,5 +6054,498 @@ class AuditSevenScreens(unittest.TestCase):
self.assertIn("1 wrong copy: backed up, replaced.", view)
class _Decided(Exception):
"""Raised where line mode has chosen its platforms and folders."""
def line_decision(args: argparse.Namespace, answer) -> tuple[list, str]:
"""(platforms, text) line mode reaches at a terminal for args: the run
stops where its platforms are known, before anything is read or written.
answer(prompt) answers each question."""
def stop(_args, platforms):
raise _Decided(platforms)
install.LOCAL.reset()
with mock.patch.object(install, "app_refusal", side_effect=stop), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install.sys.stdin, "isatty", return_value=True), \
mock.patch.object(install, "_is_terminal", return_value=True), \
mock.patch.object(install, "_ask", side_effect=answer), \
mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
contextlib.redirect_stdout(io.StringIO()) as out, \
contextlib.redirect_stderr(io.StringIO()):
try:
install._install(args, io.StringIO())
except _Decided as decided:
return decided.args[0], out.getvalue()
finally:
install.LOCAL.reset()
raise AssertionError("line mode chose nothing:\n" + out.getvalue())
def screen_decision(args: argparse.Namespace) -> tuple[str, Path]:
"""(platform, folder) the full screen reaches for args, A pressed on
whatever it offers first, as Enter is in line mode."""
install.LOCAL.reset()
with mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()):
try:
state, effects = install._tui_startup(args)
for _press in range(6):
if any(effect[0] == "fetch" for effect in effects):
return state.platform, state.dest
if state.screen == "create":
# Line mode creates a default folder where the screen asks.
state.focus["create"] = 0
effects = install.tui_update(state, Key("enter"))
finally:
install.LOCAL.reset()
raise AssertionError(f"the screen chose nothing: {state.stack}")
class LineModeAndScreenDecideAlike(unittest.TestCase):
"""tui_audit_8 P1-1, P2-1 and code_audit_15 P2-2: line mode looked at
this machine alone, took this computer's default folder with a card in
the reader or nothing found, and the screen put RetroArch under A for a
KNULLI card named with --dest. The same inputs now give the same
platform and folder on both, A or Enter taking what each offers first."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
for patcher in (mock.patch.dict(install._DETECTED_OS, {}),
mock.patch.dict(os.environ, {"HOME": str(self.root / "home"),
"XDG_CONFIG_HOME": str(self.root / "cfg"),
"RETROBIOS_OS": "linux"})):
patcher.start()
self.addCleanup(patcher.stop)
self.list = self.root / "retroarch.json"
self.list.write_text(json.dumps({"manifest_version": 2, "platform": "retroarch",
"files": []}))
self.here = self.root / "pc" / "system"
self.here.mkdir(parents=True)
def card(self, name: str, system: str) -> Path:
root = self.root / "media" / name
make_card(root, system)
return root
def both(self, mounts: list, found: list, **given) -> tuple:
def answer(prompt: str) -> str:
# The first entry of a numbered list, the default of a folder.
return "1" if prompt.startswith(">") else ""
extra = {"declared_only": False, "offline": False}
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
mock.patch.object(install, "detect_platforms", return_value=found), \
mock.patch.object(install, "detect_frontends", return_value=[]):
platforms, text = line_decision(_args(**extra, **given), answer)
install._DETECTED_OS.clear()
screen = screen_decision(_args(**extra, **given))
return platforms[0], screen, text
def test_the_same_scenarios_give_the_same_platform_and_folder(self):
onion = self.card("ONION", "Onion")
muos = self.card("MUOS", "muOS")
knulli = self.card("KNULLI", "KNULLI")
bare = self.card("ONIONB", "Onion")
shutil.rmtree(bare / "BIOS")
default = install._default_dest("linux", "retroarch")
pc = [("retroarch", self.here)]
scenarios = {
"a list, an Onion card in the reader": (
[onion], [], {"manifest": self.list}, ("retroarch", onion / "BIOS")),
"a list, an Onion card without its BIOS folder": (
[bare], [], {"manifest": self.list}, ("retroarch", bare / "BIOS")),
"a list, a KNULLI card in the reader": (
[knulli], [], {"manifest": self.list}, ("retroarch", default)),
"a list, nothing here": ([], [], {"manifest": self.list}, ("retroarch", default)),
"nothing named, two cards": (
[muos, onion], [], {}, ("retroarch", muos / "MUOS" / "bios")),
"nothing named, nothing here": ([], [], {}, ("retroarch", default)),
"--dest a KNULLI card, RetroArch here": (
[knulli], pc, {"dest": knulli}, ("knulli", knulli / "bios")),
"--platform onion, its card": (
[onion, muos], pc, {"platform": "onion"}, ("retroarch", onion / "BIOS")),
}
for name, (mounts, found, given, expected) in scenarios.items():
with self.subTest(scenario=name):
line, screen, text = self.both(mounts, found, **given)
self.assertEqual(tuple(line), expected, text)
self.assertEqual(screen, expected)
self.assertNotIn("using default path", text)
def test_line_mode_says_the_card_it_found_and_names_its_folder(self):
onion = self.card("ONION", "Onion")
with mock.patch.object(install, "_mounted_cards", return_value=[onion]), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]):
_platforms, text = line_decision(_args(manifest=self.list, declared_only=False),
lambda prompt: "")
self.assertIn(f"Found Onion at {onion / 'BIOS'} (RetroArch files)", text)
self.assertNotIn("No supported platform detected", text)
def test_a_pack_gets_the_menu_and_its_folder_named(self):
"""P1-1: no menu and no folder before the copy, the pack's list
recording no system."""
from test_install_offline import _pack, PAYLOAD
downloads = self.root / "downloads"
downloads.mkdir()
pack = _pack(downloads, "RetroArch_v1_BIOS_Pack.zip", PAYLOAD)
install.LOCAL.reset()
self.addCleanup(install.LOCAL.reset)
menus = []
real = install._prompt_custom_selection
def menu(*given, **named):
with mock.patch.object(install, "_ask", return_value="q"):
try:
real(*given, **named)
except SystemExit as stop:
menus.append(stop.code)
raise
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install.sys.stdin, "isatty", return_value=True), \
mock.patch.object(install, "_is_terminal", return_value=True), \
mock.patch.object(install, "_ask", return_value=str(self.here)), \
mock.patch.object(install, "_prompt_custom_selection", menu), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._install(_args(from_pack=[pack], declared_only=False), io.StringIO())
text = out.getvalue()
self.assertEqual(menus, [130])
self.assertIn("RetroArch was not found here.", text)
self.assertIn(f"BIOS folder: {self.here}", text)
self.assertIn("Enter) install everything", text)
self.assertNotIn("c) choose systems", text)
self.assertNotIn("Copying", text)
class OneDeviceRegistry(unittest.TestCase):
"""code_audit_15 P2-1, tui_audit_8 P3-2, P3-3, P3-4: one device spelled
several ways found one system, one press took it, MinUI listed brands."""
def found(self, typed: str, os_type: str = "linux") -> set:
return {row.value[1] if row.action == "cardsystem" else row.value
for row in install.setup_list([], set(), typed, os_type)}
def test_one_spelling_per_device(self):
keys = {}
for name in install._DEVICES:
keys.setdefault(install._device_key(name), []).append(name)
self.assertEqual({key: names for key, names in keys.items() if len(names) > 1}, {})
for system, devices in install._SETUP_DEVICES.items():
with self.subTest(system=system):
self.assertEqual([name for name in devices if name not in install._DEVICES], [])
def test_every_spelling_finds_every_system_that_runs_it(self):
rk2023 = {"ArkOS", "KOLOS", "JELOS", "rocknix"}
cubexx = {"muOS", "knulli", "rocknix", "minui"}
flip2 = {"knulli", "rocknix"}
for typed, systems in (("Powkiddy RK2023", rk2023), ("RK2023", rk2023),
("rk-2023", rk2023), ("RG CubeXX", cubexx),
("RGCUBEXX", cubexx), ("Anbernic RG CubeXX", cubexx),
("Retroid Pocket Flip 2", flip2), ("flip2", flip2)):
with self.subTest(typed=typed):
self.assertEqual(self.found(typed), systems)
self.assertIn("ArkOS", self.found("Anbernic RG353V"))
self.assertTrue({"Onion", "minui", "myminui", "Allium"} <= self.found("Miyoo Mini"))
self.assertTrue({"Onion", "minui"} <= self.found("Miyoo Mini+"))
self.assertIn("minui", self.found("TrimUI Brick"))
self.assertIn("Anbernic stock firmware", self.found("rg35xx"))
self.assertIn("JELOS", self.found("rgb10 max 3"))
def test_android_is_this_machine_only_on_android(self):
self.assertNotIn("retroarch", self.found("android"))
self.assertNotIn("retroarch", self.found("retroid pocket 5"))
self.assertIn("retroarch", self.found("android", "android"))
def test_a_device_several_systems_run_is_never_taken_in_one_press(self):
out = io.StringIO()
with mock.patch.object(install, "_ask",
side_effect=["Powkiddy RK2023", "", "0", "35", "q"]), \
mock.patch.object(install, "detect_os", return_value="linux"), \
contextlib.redirect_stdout(out), self.assertRaises(SystemExit):
install._choose_platform("Which?", "retroarch")
text = out.getvalue()
self.assertIn("4 setups run on 'Powkiddy RK2023':", text)
# Enter after the matches takes none, and 0 and 35 are numbers.
self.assertEqual(text.count("Type a number from 1 to 34"), 3)
self.assertNotIn("run on '0'", text)
self.assertNotIn("run on '35'", text)
def test_the_line_menu_is_the_screen_list_numbered(self):
for setups in ([], [("retroarch", SYSTEM_DIR, "RetroArch")]):
with self.subTest(found=bool(setups)):
rows = install.setup_list(setups, set(), "", "linux")
self.assertEqual([name for name, _label in install._line_setups(setups)],
[install._row_name(row) for row in rows])
labels = dict(install._line_setups())
self.assertIn("RG28XX", labels["minui"])
self.assertNotIn("(Anbernic, Miyoo", labels["minui"])
self.assertIn("RG35XX Plus", labels["Anbernic stock firmware"])
self.assertNotIn("RetroArch files", labels["JELOS"])
def test_forty_columns_keep_the_name_whole_and_a_device_count(self):
rows = {row.label: row for row in install.setup_list([], set(), "", "linux")}
for label in ("KNULLI, SD card", "ROCKNIX, SD card", "CrossMix-OS, SD card",
"AmberELEC, SD card", "JELOS, SD card"):
with self.subTest(row=label):
line = str(install._row_line(rows[label], False, 38))
self.assertIn(label, line)
self.assertTrue(line.rstrip() != (" " + label).rstrip(), line)
self.assertTrue(str(install._row_line(rows["CrossMix-OS, SD card"], False, 38))
.rstrip().endswith("1 device"))
class AuditFifteenAndEightScreens(unittest.TestCase):
"""The P3 points of code_audit_15 and tui_audit_8 the screens show."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
patcher = mock.patch.dict(install._DETECTED_OS, {})
patcher.start()
self.addCleanup(patcher.stop)
def start(self, mounts=(), found=(), **given) -> tuple:
with mock.patch.object(install, "_mounted_cards", return_value=list(mounts)), \
mock.patch.object(install, "detect_platforms", return_value=list(found)), \
mock.patch.object(install, "detect_frontends", return_value=[]):
return install.tui_start(_args(**given), "linux")
def test_a_card_found_by_its_files_gets_its_folder_without_a_question(self):
"""tui_audit_8 P3-12: Create the folder? for the Onion card's own
BIOS, which line mode created."""
onion = self.root / "ONIONB"
make_card(onion, "Onion")
shutil.rmtree(onion / "BIOS")
state, effects = self.start([onion], platform="onion")
self.assertEqual(effects, [("fetch", "retroarch")])
self.assertEqual(state.dest, onion / "BIOS")
self.assertIn("BIOS is created on ONIONB", state.card_note)
def test_a_refused_dest_says_the_remedy_that_works(self):
"""code_audit_15 P3-3, P3-4."""
rocknix = self.root / "ROCKNIX"
make_card(rocknix, "ROCKNIX")
stick = self.root / "STICK"
(stick / "photos").mkdir(parents=True)
for named, plat, dest, words, never in (
("knulli", "knulli", rocknix / "roms" / "bios", "--platform rocknix", "<card>"),
("onion", "retroarch", stick, "Name the top folder of the Onion card",
"<card>/BIOS")):
with self.subTest(named=named):
with contextlib.redirect_stderr(io.StringIO()) as err, \
self.assertRaises(SystemExit):
install.card_dest(named, plat, dest)
self.assertIn(words, err.getvalue())
self.assertNotIn(never, err.getvalue())
minui = self.root / "MINUI"
make_card(minui, "MinUI")
(minui / "Bios").mkdir()
state = install.TuiState()
install._card_status(state, install.card_choice("minui", "minui", minui / "Bios"),
given=True)
self.assertIn(f"Run it again with --dest {minui}.", state.status)
def test_wsl_names_drives_in_line_mode_messages(self):
"""code_audit_15 P3-7: /mnt/f in a refusal where D:\\ was typed."""
mnt = self.root / "mnt"
make_card(mnt / "f", "muOS")
(mnt / "g" / "photos").mkdir(parents=True)
with mock.patch.dict(install._WSL, {"active": True, "root": str(mnt)}), \
mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
with contextlib.redirect_stderr(io.StringIO()) as err, \
self.assertRaises(SystemExit):
install.card_dest("onion", "retroarch", mnt / "f")
self.assertIn("F:\\ is a muOS card", err.getvalue())
note = install.card_choice("knulli", "knulli", mnt / "g").note
self.assertIn("G:\\bios", note)
self.assertNotIn(str(mnt), note)
self.assertEqual(install._last_name(str(mnt / "g" / "old")), "old")
def test_choose_the_card_below_only_when_one_is_mounted(self):
"""tui_audit_8 P3-6."""
muos = self.root / "MUOS"
make_card(muos, "muOS")
stick = self.root / "USBSTICK"
(stick / "photos").mkdir(parents=True)
with mock.patch.object(install, "_mounted_cards", return_value=[muos, stick]):
state, _effects = self.start([muos, stick], platform="onion")
rows = install.tui_rows(state)
state.focus["folder"] = next(index for index, row in enumerate(rows)
if row.action == "browse")
install.tui_update(state, Key("enter"))
self.assertEqual(state.screen, "browse")
self.assertIn("No Onion card is mounted", state.status)
focused = install.tui_rows(state)[state.focus["browse"]]
self.assertEqual(focused.value, stick)
def test_type_a_path_says_f1_and_f1_says_more(self):
"""tui_audit_8 P2-2."""
muos = self.root / "MUOS"
make_card(muos, "muOS")
with mock.patch.object(install, "_mounted_cards", return_value=[]):
state, _effects = self.start(platform="onion")
stick = self.root / "STICK"
stick.mkdir()
state.stack.append("path")
install._card_status(state, install.card_choice("Onion", "retroarch", stick))
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertIn("F1 says more.", text)
self.assertNotIn("? says more", text)
install.tui_update(state, Key("f1"))
self.assertTrue(state.help)
help_text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertIn("Onion keeps .tmp_update at the root of its card", help_text)
def test_batocera_on_a_computer_offers_its_share(self):
"""tui_audit_8 P2-4."""
stick = self.root / "SHARE"
stick.mkdir()
state, _effects = self.start([stick])
state.stack = ["platforms"]
state.focus["platforms"] = install._platform_focus(state, "batocera")
with mock.patch.object(install, "_mounted_cards", return_value=[stick]):
install.tui_update(state, Key("enter"))
rows = install.tui_rows(state)
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertEqual(state.screen, "folder")
self.assertNotIn("/userdata/bios", text)
self.assertIn("SHARE drive", text)
self.assertEqual([row.action for row in rows], ["share", "browse", "type"])
self.assertEqual(rows[0].value, stick / "bios")
def test_the_first_screen_bar_says_quit(self):
"""tui_audit_8 P3-10."""
state, _effects = self.start()
self.assertEqual(state.stack, ["platforms"])
self.assertIn("B/Esc Quit", install._TUI_VIEWS["platforms"](state, 80, 24)[2])
state.stack = ["setup", "platforms"]
self.assertIn("B/Esc Back", install._TUI_VIEWS["platforms"](state, 80, 24)[2])
def test_a_pad_result_prints_no_command(self):
"""tui_audit_8 P3-9: a shell command on a controller-only screen."""
state = _installed_state(self.root / "bios")
state.summary["kept"] = [{"folder": str(self.root / "kept"), "files": 1}]
state.pad = "muxterm"
_title, lines = install.tui_outcome(state)
text = " ".join(lines)
self.assertNotIn("install.py", text)
self.assertIn("kept in", text)
state.pad = "keys"
self.assertIn("Same choice without questions:", install.tui_outcome(state)[1])
def test_the_checking_screen_is_titled_as_the_plan(self):
"""code_audit_15 P3-9.1."""
state = install.TuiState()
state.platform, state.dest = "retroarch", self.root / "bios"
install._DETECTED_OS[("retroarch", state.dest)] = "ArkOS"
state.stack = ["checking"]
text = " ".join(install.tui_view(state, 80, 24))
self.assertIn("ArkOS (RetroArch files):", text)
def test_the_backup_line_survives_forty_columns(self):
"""tui_audit_8 P3-5: under the pack and copy split it was cut."""
state = checked_state()
state.found = [found("gba_bios.bin", "/media/old")]
lines = [str(line) for line in install._plan_lines(state.check, state.dest, state)]
self.assertTrue(any("copied from this machine" in line for line in lines), lines)
counts = next(index for index, line in enumerate(lines) if " in place, " in line)
self.assertTrue(lines[counts + 1].startswith("The 1 wrong copy is replaced"), lines)
def test_an_empty_folder_is_said_once(self):
"""tui_audit_8 P3-7."""
old = self.root / "USBSTICK"
old.mkdir()
state = checked_state()
state.from_folders = [old]
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
lines = [str(line) for line in install._plan_lines(state.check, state.dest,
state)]
said = [line for line in lines if "USBSTICK" in line]
self.assertEqual(said, ["USBSTICK holds none of these files: run the installer again "
"with a pack (--from-pack) or without --offline."])
def test_amberelec_keeps_its_files_on_the_games_part(self):
"""tui_audit_8 P3-13."""
self.assertEqual(install.card_where("AmberELEC", ""),
"on the games part of its SD card")
files = [{"dest": "bios/a.bin"}, {"dest": "bios/Machines/b.xml"}]
self.assertEqual(install.shown_bios_folder(self.root, files), self.root / "bios")
self.assertEqual(install.shown_bios_folder(self.root, [{"dest": "a.bin"}]),
self.root)
def test_a_save_directory_left_out_has_one_wording(self):
"""code_audit_15 P3-9.3, tui_audit_8 P3-8."""
files = [{"dest": "keys.txt", "root": "save", "sha1": "0" * 40, "size": 1}]
with contextlib.redirect_stdout(io.StringIO()) as out:
install.place_save_entries(files, self.root / "BIOS",
{"platform": "retroarch", "save_directory": {
"folder": "saves", "by_core": False},
"base_destination": "system"},
None, "RetroArch")
self.assertIn("left out: its place is not known. Name it with --save-dir.",
out.getvalue())
class CardBoundaries(unittest.TestCase):
"""code_audit_15 P3-2, P3-9.5: three folders at most, a bind mount not
seen, a folder named MUOS taken for a card, ismount unprobed."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
def choice(self, folder: Path, points=frozenset()) -> install.CardChoice:
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "_mount_points", return_value=points):
return install.card_choice("knulli", "knulli", folder)
def test_every_folder_up_to_the_mount_is_read(self):
muos = self.root / "MUOS"
make_card(muos, "muOS")
deep = muos / "a" / "b" / "c" / "d"
deep.mkdir(parents=True)
self.assertIn("muOS", self.choice(deep).refused)
def test_a_bind_mount_inside_a_card_is_its_own_drive(self):
muos = self.root / "MUOS"
make_card(muos, "muOS")
stick = muos / "x"
stick.mkdir()
self.assertTrue(self.choice(stick).refused)
choice = self.choice(stick, frozenset({str(stick)}))
self.assertEqual((choice.refused, choice.folder), ("", stick / "bios"))
def test_a_folder_named_muos_beside_it_is_no_card(self):
backups = self.root / "stage" / "backups"
(backups / "MUOS").mkdir(parents=True)
(backups / "knulli-new").mkdir()
choice = self.choice(backups / "knulli-new")
self.assertEqual(choice.refused, "")
self.assertEqual(choice.folder, backups / "knulli-new" / "bios")
def test_mount_points_are_probed(self):
with mock.patch.object(install, "_probed", wraps=install._probed) as probed, \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
mock.patch.object(install, "_mounted_cards", return_value=[]):
install._card_boundary(self.root)
self.assertTrue(probed.called)
points = install._mount_points()
if Path("/proc/self/mountinfo").exists():
self.assertIn("/", points)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -81,7 +81,7 @@ It opens a menu:
- **Right folder?** Press Enter.
- **Wrong folder?** Choose **Another setup or folder**, then browse to yours.
- **A handheld's SD card?** Type its name (Miyoo Mini).
- **A handheld's SD card?** There, type its name (Miyoo Mini).
Arrows move, Enter chooses, Esc goes back, as A and B on a handheld.
+26 -16
View File
@@ -109,7 +109,9 @@ SHA-1 and size, and every file is checked against it before it takes its
place. A data tree (Dolphin's `Sys/`, PPSSPP's assets) comes from the pack
too, not from its source archive. `--target` reads a core list from the
network and is refused with `--from-pack`. A board found on the device is not
applied either: every board's files are installed.
applied either: every board's files are installed. At a terminal the run
finds a card mounted here, asks the folder when nothing is found, and names
the BIOS folder on its menu before anything is copied.
`--manifest FILE` reads the file list from a copy of `install/<platform>.json`
instead of downloading it. Alone, the files still come from the repository.
@@ -125,7 +127,8 @@ internet instead of offering to fetch them. The run installs the copies found
on this machine and downloads nothing. The files left out are missing, not
failed: it ends `Finished with files missing` (code 1), or `Nothing installed`
(code 2) when nothing was installed, at a terminal, without questions and on
the full screen alike. The advice that follows names a pack or a run without
the full screen alike. The `summary` event counts them in `pending`, never in
`failed`. The advice that follows names a pack or a run without
`--offline`, and no `--from` folder already read. A command it suggests keeps
`--offline` when `RETROBIOS_INSTALL_OFFLINE` set it, sudo not passing that on.
The `--plain` menu then offers `f) copy from a folder or an old card`, and its
@@ -166,16 +169,16 @@ install starts.
| Action | Keyboard | Controller |
|--------|----------|------------|
| Move | Up and Down, `k` and `j` | D-pad Up and Down |
| Page | PgUp and PgDn | L1 and R1 |
| Page | PgUp and PgDn | L1 and R1 (muxterm: D-pad Left and Right) |
| First, last | Home and End, `g` and `G` | |
| Continue, open, choose | Enter, and Space outside a list of ticks | A, Start |
| Tick, untick | Space, Right, Left | X, D-pad Right and Left |
| Back | Esc, Backspace | B, Select |
| Options | `o`, Tab | Y |
| Filter a long list | `/` then type, Enter on a lone match chooses it | none: a pad cannot type (the setup list puts the SD cards after RetroArch) |
| Continue, open, choose | Enter, and Space outside a list of ticks | A, Start (muxterm: Start) |
| Tick, untick | Space, Right, Left | X, D-pad Right and Left (muxterm: Y, Miyoo: A) |
| Back | Esc, Backspace | B, Select (Miyoo: R2) |
| Options | `o`, Tab | Y (Miyoo: L2, muxterm: none) |
| Filter a long list | `/` then type, Enter on a lone match chooses it | muxterm: Select shows a keyboard. Elsewhere none (the setup list puts the SD cards after RetroArch) |
| Keys of the screen | `?`, F1 | |
| Stop an install | Esc, `q`, Ctrl+C | B, Start+Select |
| Quit | `q`, Ctrl+C | B on the first screen |
| Stop an install | Esc, `q`, Ctrl+C | B, Start+Select (Miyoo: R2) |
| Quit | `q`, Ctrl+C | B on the first screen (Miyoo: R2) |
The bottom line of each screen names what A does on the row under the cursor,
in the names of what is held. On a computer it names the keys (`Enter Install`,
@@ -699,7 +702,7 @@ what the options name, everything by default.
| Option | Effect |
|--------|--------|
| <span id="option-platform"></span>`--platform NAME` | Install for this platform instead of the detected one. A system `--list-platforms` names under its own files (`muos`, `arkos`) stands for the file list it gets, and the run says which. Unknown names are refused with the closest ones |
| <span id="option-dest"></span>`--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is. A folder that does not exist is named with the nearest one that does, then created. When more than its last level is missing (a mistyped path), a run at a terminal asks first, and the answer defaults to no |
| <span id="option-dest"></span>`--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is. A folder that does not exist is named with the nearest one that does, then created, except for a system that runs from its SD card, which refuses it. When more than its last level is missing (a mistyped path), a run at a terminal asks first, and the answer defaults to no |
| <span id="option-target"></span>`--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given |
| <span id="option-every-board"></span>`--every-board` | Install the files of every board instead of the one the system names, for a card that moves between devices |
| <span id="option-installed-cores"></span>`--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie). See [Installed cores](#installed-cores) |
@@ -939,7 +942,8 @@ The filter takes a device name, and so does the `--plain` menu, which lists
the setups it matches by number. With nothing found here, the SD card rows
come right after RetroArch: Onion is four presses away with L1 and R1 at
40x12, where it was row 30 of 34. That row, `--platform onion` and the menu
all ask for the card: one card of that system mounted is taken, otherwise
all ask for the card: one card of that system mounted when the run starts is
taken, otherwise
the screen opens **Where is the SD card?**, which lists only that system's
cards and offers **Look again**, and `--plain` asks `Card root`, listing the
same cards by number and naming the one picked and its folder. The RetroArch
@@ -971,8 +975,14 @@ home folder or above the default, no default is taken: the frontend's own
configuration there may name another folder, so the run asks for `--dest`.
With nothing detected, the setup screen lists the platforms to choose from,
and `--plain` gives the same list numbered. With several detected, the setup
screen lists each one, and `--plain` asks which. Both need a terminal: piped into a script
and `--plain` gives the same list numbered, in the same order. Both find the
SD cards mounted here, the screen under **Found on this machine**. With
several found, the setup screen lists each one, and `--plain` asks which. A
platform named, or read from a list or a pack, and found nowhere gets its
folder asked, its usual one under Enter, the mounted drives numbered. On a
computer Batocera and Recalbox have none: the `bios` folder of their SHARE
drive or network share is theirs. A device several systems run lists them
all. Both need a terminal: piped into a script
with no platform to install for, it prints the manual invocation, in the form
the run was started with (the one-line command for a one-liner user), and
exits 1.
@@ -1333,7 +1343,7 @@ fields, the installer refuses to narrow rather than install everything.
| Code | Meaning |
|------|---------|
| `0` | everything asked for is installed or already up to date |
| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 (nothing was run), an unusable base URL, an unknown platform or target, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, no platform to install for in a non-interactive run, some files failed or were left out by `--offline` while others installed, `--check` found files to fetch, a `--from-pack` pack that cannot be read, lacks a part or is for another platform, a `--from` folder that does not exist or cannot be read, a `--dest` that is not the card of the system named (another system's card, a folder inside one, or a drive with none of its files where its card always has them), `--restore` could not put a file back, or `--add-app` could not put every file of the app on the card |
| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 (nothing was run), an unusable base URL, an unknown platform or target, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, no platform to install for in a non-interactive run, some files failed or were left out by `--offline` while others installed, `--check` found files to fetch, a `--from-pack` pack that cannot be read, lacks a part or is for another platform, a `--from` folder that does not exist or cannot be read, a `--dest` that is not the card of the system named (another system's card, a folder inside one, a drive with none of its files where its card always has them, or a folder that does not exist), `--restore` could not put a file back, or `--add-app` could not put every file of the app on the card |
| `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one), or `--offline` left every file out, the drive has too little free space for the download (checked before any folder is created), or the question about a folder that does not exist, named with `--dest` or typed at the folder question, was answered no. Also an option the installer refused before any download: an unknown option, `--jobs` outside 1 to 32, `--target` with `--every-board` or `--from-pack`, `--target` or `--list-targets` with `--offline` (or `RETROBIOS_INSTALL_OFFLINE` set), which read a core list from the network, `--add-app` with `--check`, `--dry-run`, a listing, `--from-pack` or `--offline`. The message then starts with `install.py: error:`. Also `--add-app` for a folder on no card that takes the app, said in an `Error:` line and an `app` event before anything is written. With `--restore`, a folder that is not a dated folder inside `retrobios-backup` |
| `130` | stopped with Ctrl+C, or with `q`, Ctrl+C or Ctrl+D at a question, or the full screen closed with no button pressed (`RETROBIOS_IDLE_EXIT`) before an install. Run the same command again to carry on |
| `141` | the output was piped to a reader that closed early (`--list-platforms \| head`) |
@@ -1376,7 +1386,7 @@ other schemas under `api/v1/schemas/`.
| `start` | `schema`, `installer`, `mode` | First line. `schema` is 1, `installer` the identity `--version` prints, `mode` one of `install`, `check`, `dry-run`, `list` |
| `unsearched` | `folder`, `reason`, `message` | Before `detect`, once for each folder the search for setups could not look in, this user being refused it. What it would hold counts as absent, and the other places are still searched: `detect` then lists what they hold, and an empty `platforms` after an `unsearched` line means nothing was found where the installer could look. `reason` says why in words (`Permission denied`), `message` says it in a sentence |
| `detect` | `os`, `os_name`, `platforms` | Once the platforms are known. `os_name` comes from `/etc/os-release`. `platforms` lists `{platform, name, bios_dir}`, and is empty when nothing supported was found |
| `error` | `stage`, `reason`, `platform`, `message` | The file list could not be read (`stage` is `manifest`): `reason` is `network`, `not_found` or `invalid`, `clock` when the server's certificate was refused and the system clock reads a date before the installer was written, `no_ca` when the machine holds no list of trusted certificates (`SSL_CERT_FILE` names one), or `offline` when `--offline` forbids the request. A folder named with `--from` cannot be read (`stage` is `from`, `reason` `not_found`, `not_a_folder` for a file that is no ZIP, `unreadable`, or `system` for the root of the system, `/proc`, `/sys` or `/dev`). The BIOS folder sits under a folder this user cannot open (`stage` is `dest`, `reason` `unreadable`), and the run ends with code 1 before anything is written. Two systems are detected on one machine and no `--dest` names the folder (`stage` is `detect`, `reason` `conflict`). The platform named with `--platform` is found nowhere and no folder can be assumed for it: a card system with no card named, a portable emulator, or no usual folder on this system or one set aside behind a folder this user cannot open (`stage` is `detect`, `reason` `not_found`). `message` says it in words |
| `error` | `stage`, `reason`, `platform`, `message` | The file list could not be read (`stage` is `manifest`): `reason` is `network`, `not_found` or `invalid`, `clock` when the server's certificate was refused and the system clock reads a date before the installer was written, `no_ca` when the machine holds no list of trusted certificates (`SSL_CERT_FILE` names one), or `offline` when `--offline` forbids the request. A folder named with `--from` cannot be read (`stage` is `from`, `reason` `not_found`, `not_a_folder` for a file that is no ZIP, `unreadable`, or `system` for the root of the system, `/proc`, `/sys` or `/dev`). The BIOS folder sits under a folder this user cannot open (`stage` is `dest`, `reason` `unreadable`), and the run ends with code 1 before anything is written. Two systems are detected on one machine and no `--dest` names the folder (`stage` is `detect`, `reason` `conflict`). The platform named with `--platform` is found nowhere and no folder can be assumed for it: a card system with no card named, a portable emulator, or no usual folder on this system or one set aside behind a folder this user cannot open (`stage` is `detect`, `reason` `not_found`). A `--dest` refused as the card of the system named carries the same `stage` and `reason`: another system's card, a folder inside a card, a drive with none of the files its card always has, or a folder that does not exist. `message` says it in words, and is the only thing that tells these cases apart |
| `scanned` | `folder`, `files`, `truncated`, `other_drives` | Each folder named with `--from`, once read and before `detect`. `files` counts the files found, `truncated` is true when the search stopped at 200,000 of them, and `other_drives` lists the folders below it on another drive, which were not entered |
| `checking` | `files_done`, `files_total` | While the files already in the folder are read and hashed, at most every half second and once at the end. `files_total` counts the files found in place |
| `file` | `platform`, `dest`, `systems`, `size`, `state` | Each file of the list after the check. `state` is `present` (hash verified), `wrong_hash` or `missing` |