mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
chore: merge origin/main
This commit is contained in:
commit
7d23d8f40b
49 files changed
+501
-117
No files matched your search
@@ -28,6 +28,7 @@ on:
|
||||
- "scripts/generate_readme.py"
|
||||
- "scripts/generate_site.py"
|
||||
- "scripts/hashing.py"
|
||||
- "scripts/indexnow.py"
|
||||
- "scripts/largefiles.py"
|
||||
- "scripts/launchers.py"
|
||||
- "scripts/manifestselect.py"
|
||||
@@ -60,6 +61,7 @@ on:
|
||||
- "scripts/siterender.py"
|
||||
- "scripts/retired_files.py"
|
||||
- "scripts/sitesources.py"
|
||||
- "scripts/sitesteps.py"
|
||||
- "scripts/sitestarlight.py"
|
||||
- "scripts/sitewrite.py"
|
||||
- "scripts/slot.py"
|
||||
@@ -112,6 +114,8 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
indexnow: ${{ steps.indexnow.outputs.payload }}
|
||||
# Node's compile cache writes under /tmp on every node run, the build
|
||||
# and the search gate alike.
|
||||
env:
|
||||
@@ -184,6 +188,14 @@ jobs:
|
||||
- name: Require committed README and CONTRIBUTING to be current
|
||||
run: git diff --exit-code -- README.md CONTRIBUTING.md
|
||||
|
||||
# The pages whose words this deploy changes, read against the
|
||||
# digests the live site still serves. A failure here only loses the
|
||||
# notice: search engines find the pages on their next visit.
|
||||
- name: List the pages this deploy changes
|
||||
id: indexnow
|
||||
continue-on-error: true
|
||||
run: python scripts/indexnow.py --dist website/dist --output "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
|
||||
|
||||
@@ -202,3 +214,17 @@ jobs:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|
||||
|
||||
# IndexNow: Bing, Yandex, Seznam and Naver fetch the listed pages
|
||||
# soon after, so an assistant searching through them reads this build.
|
||||
- name: Tell search engines which pages changed
|
||||
if: needs.build.outputs.indexnow != ''
|
||||
continue-on-error: true
|
||||
env:
|
||||
PAYLOAD: ${{ needs.build.outputs.indexnow }}
|
||||
run: |
|
||||
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H 'Content-Type: application/json; charset=utf-8' \
|
||||
--data "$PAYLOAD" https://api.indexnow.org/indexnow)
|
||||
echo "IndexNow answered $code"
|
||||
case "$code" in 200|202) ;; *) echo "::warning::IndexNow answered $code"; exit 1 ;; esac
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Android</title><path d="M18.44 5.559q-1.015 1.748-2.028 3.498-.055-.023-.111-.043a12.1 12.1 0 0 0-8.68.033C7.537 8.897 5.868 6.026 5.6 5.56a1 1 0 0 0-.141-.19 1.104 1.104 0 0 0-1.768 1.298c1.947 3.37-.096-.216 1.948 3.36.017.03-.495.263-1.393 1.017C2.9 12.176.452 14.772 0 18.99h24a11.7 11.7 0 0 0-.746-3.068 12.1 12.1 0 0 0-2.74-4.184 12 12 0 0 0-2.131-1.687c.66-1.122 1.312-2.256 1.965-3.385a1.11 1.11 0 0 0-.008-1.12 1.1 1.1 0 0 0-.852-.532c-.522-.054-.939.313-1.049.545m-.04 8.46c.395.593.324 1.331-.156 1.65-.48.32-1.188.1-1.582-.493s-.324-1.33.156-1.65c.473-.316 1.182-.11 1.582.494m-11.193-.492c.48.32.55 1.058.156 1.65-.394.593-1.103.815-1.584.495-.48-.32-.55-1.058-.156-1.65.4-.603 1.109-.811 1.584-.495"/></svg>
|
||||
|
After Width: | Height: | Size: 788 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><rect width="10" height="14" x="3" y="8" rx="2"/><path d="M5 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2h-2.4M8 18h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 314 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Linux</title><path d="M12.504 0q-.232 0-.48.021c-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.4.4 0 0 0-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071s1.592-.536 2.257-1.306c.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.36.36 0 0 0-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139m.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105l-.004-.021-.004-.024a1.8 1.8 0 0 1-.15.706.95.95 0 0 1-.213.335 1 1 0 0 0-.088-.042c-.104-.045-.198-.064-.284-.133a1.3 1.3 0 0 0-.22-.066c.05-.06.146-.133.183-.198q.08-.193.088-.402v-.02a1.2 1.2 0 0 0-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 0 0-.205.334 1.2 1.2 0 0 0-.09.4v.019q.002.134.02.267c-.193-.067-.438-.135-.607-.202a2 2 0 0 1-.018-.2v-.02a1.8 1.8 0 0 1 .15-.768 1.08 1.08 0 0 1 .43-.533 1 1 0 0 1 .594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2q.018-.136.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.6.6 0 0 0-.166-.267.25.25 0 0 0-.183-.064h-.021c-.071.006-.13.04-.186.132a.55.55 0 0 0-.12.27 1 1 0 0 0-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268q.014.014.034.024c-.07.057-.117.07-.176.136a.3.3 0 0 1-.131.068 2.6 2.6 0 0 1-.275-.402 1.8 1.8 0 0 1-.155-.667 1.8 1.8 0 0 1 .08-.668 1.4 1.4 0 0 1 .283-.535c.128-.133.26-.2.418-.2m1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.57.57 0 0 1 .016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.1 1.1 0 0 1-.448-.067 4 4 0 0 1-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71q-.104-.403.193-.6c.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.7 2.7 0 0 0-.402-.533 1.5 1.5 0 0 0-.275-.333c.182 0 .338-.03.465-.067a.62.62 0 0 0 .314-.334c.108-.267 0-.697-.345-1.163s-.931-.995-1.788-1.521c-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.1 8.1 0 0 1 .647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335q.058.005.11.006c.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01q.08 0 .157.014c.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602q-.553-.1-.723-.4c-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336m7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8 8 0 0 1-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-Line truncated
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 828 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -8,6 +8,13 @@
|
||||
"owner": "AmberELEC",
|
||||
"licence": "GitHub organisation avatar of AmberELEC"
|
||||
},
|
||||
{
|
||||
"file": "android.svg",
|
||||
"platform": "android",
|
||||
"source": "https://raw.githubusercontent.com/simple-icons/simple-icons/7185d367013b99211c2728d5007833a922ec12c7/icons/android.svg",
|
||||
"owner": "Google",
|
||||
"licence": "Android robot, file from simple-icons, CC0-1.0"
|
||||
},
|
||||
{
|
||||
"file": "batocera.svg",
|
||||
"platform": "batocera",
|
||||
@@ -36,6 +43,13 @@
|
||||
"owner": "EmuELEC",
|
||||
"licence": "GitHub organisation avatar of EmuELEC"
|
||||
},
|
||||
{
|
||||
"file": "iphone-and-ipad.svg",
|
||||
"platform": "iphone-and-ipad",
|
||||
"source": "https://raw.githubusercontent.com/lucide-icons/lucide/a1b2ce5b7b6a09da2f2f61f682b299f69effb057/icons/tablet-smartphone.svg",
|
||||
"owner": "Lucide",
|
||||
"licence": "icon from lucide-icons, ISC"
|
||||
},
|
||||
{
|
||||
"file": "knulli.webp",
|
||||
"platform": "knulli",
|
||||
@@ -50,6 +64,20 @@
|
||||
"owner": "Lakka (libretro)",
|
||||
"licence": "file from libretro/retroarch-assets, CC-BY-4.0"
|
||||
},
|
||||
{
|
||||
"file": "linux-handhelds.svg",
|
||||
"platform": "linux-handhelds",
|
||||
"source": "https://raw.githubusercontent.com/simple-icons/simple-icons/521c96fd04b0ea93034db8715eda5a4de27a58bb/icons/linux.svg",
|
||||
"owner": "Larry Ewing",
|
||||
"licence": "Tux, file from simple-icons, CC0-1.0"
|
||||
},
|
||||
{
|
||||
"file": "minui.webp",
|
||||
"platform": "minui",
|
||||
"source": "https://raw.githubusercontent.com/shauninman/MinUI/7c8b818e869302780c4083b8a9d6309b6d4a9ccd/skeleton/EXTRAS/Tools/m17/Bootlogo.pak/logo.bmp",
|
||||
"owner": "Shaun Inman",
|
||||
"licence": "MinUI boot logo, repository states no licence"
|
||||
},
|
||||
{
|
||||
"file": "misterfpga.webp",
|
||||
"platform": "misterfpga",
|
||||
@@ -57,6 +85,13 @@
|
||||
"owner": "MiSTer FPGA",
|
||||
"licence": "GitHub organisation avatar of MiSTer-devel"
|
||||
},
|
||||
{
|
||||
"file": "myminui.webp",
|
||||
"platform": "myminui",
|
||||
"source": "https://raw.githubusercontent.com/Turro75/MyMinUI/d5ab4f9f83280f4316cb070c529299cb81ccc473/workspace/h700/boot/bootlogo.png",
|
||||
"owner": "Turro75",
|
||||
"licence": "MyMinUI boot screen, repository states no licence"
|
||||
},
|
||||
{
|
||||
"file": "nextui.webp",
|
||||
"platform": "nextui",
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
|
||||
# the site's build rewrites this line there (scripts/sitebootstrap.py).
|
||||
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
$defaultInstallSha256 = "d7434d9586a2bcf5a3122604f0a07dff1a72297d5e3a4237fedf522deb9673b7"
|
||||
$defaultInstallSha256 = "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+3
-3
@@ -379,7 +379,7 @@ COLOUR_ROLES = {
|
||||
# zero is no news and stays plain: "0 missing" is not a warning.
|
||||
_STATUS_WORDS = tuple((re.compile(pattern), role) for pattern, role in (
|
||||
(r"(?<!of )\b[1-9][\d,]* (?:files? )?(?:installed|in place|already in place"
|
||||
r"|ok|copied|downloaded)\b|\bDone\b|\bEverything is in place\b|^ *ok\b", "success"),
|
||||
r"|ok|copied|downloaded)\b|\bDone\b|\bEvery file on the list is in place\b|^ *ok\b", "success"),
|
||||
(r"(?<!of )\b[1-9][\d,]* (?:files? )?(?:failed|refused)\b|\bFAILED\b|\bfailed:"
|
||||
r"|\bFinished with errors\b|\bNothing was installed\b|\bSomething went wrong\b",
|
||||
"failure"),
|
||||
@@ -6385,7 +6385,7 @@ _INVOCATION = Invocation()
|
||||
|
||||
# What a run says when every file is in place, on the screen and in line
|
||||
# mode: the site's "How to check" quotes it (scripts/generate_site.py).
|
||||
EVERYTHING_IN_PLACE = "Everything is in place."
|
||||
EVERYTHING_IN_PLACE = "Every file on the list is in place."
|
||||
# The plan's two rows a beginner reads first: the README and the site name
|
||||
# them from here, so they say what the screen shows.
|
||||
TUI_INSTALL_ROW = "Install"
|
||||
@@ -21461,7 +21461,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
|
||||
else "")
|
||||
if not need:
|
||||
headline = (f"{_plural(data, 'data file')} to fetch{data_size}" if data
|
||||
else "Everything is in place")
|
||||
else EVERYTHING_IN_PLACE.rstrip("."))
|
||||
elif lacking and not fetch and not copied:
|
||||
# The data files too, when the pack holds none of them: "8252
|
||||
# files" said nothing of the 2636 it cannot give either.
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
|
||||
# curl or wget stop on a cut transfer before the hash is read: the site's
|
||||
# build rewrites this line there (scripts/sitebootstrap.py).
|
||||
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
DEFAULT_INSTALL_SHA256="d7434d9586a2bcf5a3122604f0a07dff1a72297d5e3a4237fedf522deb9673b7"
|
||||
DEFAULT_INSTALL_SHA256="4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1130193,
|
||||
"sha256": "d7434d9586a2bcf5a3122604f0a07dff1a72297d5e3a4237fedf522deb9673b7",
|
||||
"size": 1125299,
|
||||
"sha256": "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1183820,
|
||||
"sha256": "afca7d7f17f2c662448d57bdce1013740c735678a09fab637dc8fff44709f7b7"
|
||||
"size": 1178926,
|
||||
"sha256": "e323c16d792945916f4352442ee7bcfcb4194f13e50d70150b05684adf2d0d88"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1130193,
|
||||
"sha256": "d7434d9586a2bcf5a3122604f0a07dff1a72297d5e3a4237fedf522deb9673b7",
|
||||
"size": 1125299,
|
||||
"sha256": "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1130193,
|
||||
"sha256": "d7434d9586a2bcf5a3122604f0a07dff1a72297d5e3a4237fedf522deb9673b7",
|
||||
"size": 1125299,
|
||||
"sha256": "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
+14
-2
@@ -29,7 +29,7 @@ install_base: https://abdess.github.io/retrobios
|
||||
trust_items:
|
||||
- It checks each file before saving it.
|
||||
- It writes in the BIOS folder. For some emulators it also adds files to the save folder.
|
||||
- It never changes or deletes a game or a save.
|
||||
- It never changes or deletes a game or save file.
|
||||
- >-
|
||||
It keeps any file it replaces in a `retrobios-backup` folder, next to the BIOS folder.
|
||||
`--restore` puts it back.
|
||||
@@ -73,14 +73,24 @@ device_groups:
|
||||
# ayntec.com/products/ayn-odin-3: "Android 15".
|
||||
text: Retroid, AYN Odin and other Android handhelds
|
||||
link: platforms/retroarch.md#where-the-files-go
|
||||
logo: https://raw.githubusercontent.com/simple-icons/simple-icons/7185d367013b99211c2728d5007833a922ec12c7/icons/android.svg
|
||||
logo_owner: Google
|
||||
logo_licence: Android robot, file from simple-icons, CC0-1.0
|
||||
- name: Linux handhelds
|
||||
text: ArkOS, muOS, Onion, spruceOS
|
||||
link: platforms/linux-handhelds.md
|
||||
logo: https://raw.githubusercontent.com/simple-icons/simple-icons/521c96fd04b0ea93034db8715eda5a4de27a58bb/icons/linux.svg
|
||||
logo_owner: Larry Ewing
|
||||
logo_licence: Tux, file from simple-icons, CC0-1.0
|
||||
# No terminal runs there: the files are copied in, each app its own way
|
||||
# (which-pack.md, section iPhone and iPad, each step cited).
|
||||
- name: iPhone and iPad
|
||||
text: RetroArch, Delta, Provenance
|
||||
link: which-pack.md#iphone-and-ipad
|
||||
# A phone and a tablet, no brand: Apple's guidelines allow no use of its logo.
|
||||
logo: https://raw.githubusercontent.com/lucide-icons/lucide/a1b2ce5b7b6a09da2f2f61f682b299f69effb057/icons/tablet-smartphone.svg
|
||||
logo_owner: Lucide
|
||||
logo_licence: icon from lucide-icons, ISC
|
||||
fpga:
|
||||
label: MiSTer FPGA
|
||||
description: >-
|
||||
@@ -746,7 +756,7 @@ guides:
|
||||
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
|
||||
logo_licence: MinUI boot logo, repository states no licence
|
||||
nextui:
|
||||
summary: A game menu for TrimUI handhelds, based on MinUI, on the SD card.
|
||||
# LoveRetro/NextUI README (5291344b) lines 37, 89-94.
|
||||
@@ -771,6 +781,8 @@ guides:
|
||||
summary: A game menu for Anbernic and Miyoo handhelds, based on MinUI, on the SD card.
|
||||
# Turro75/MyMinUI README (72ed4f3d) lines 9-35.
|
||||
runs_on: Anbernic, Miyoo handhelds
|
||||
logo_owner: Turro75
|
||||
logo_licence: MyMinUI boot screen, repository states no licence
|
||||
folders:
|
||||
- where: On the SD card
|
||||
path: '`Bios/<TAG>/`, one folder per console'
|
||||
|
||||
@@ -1069,6 +1069,10 @@ platforms:
|
||||
# The card root: minarch hands each core SDCARD_PATH/Bios/<TAG>
|
||||
# (workspace/all/minarch/minarch.c:2943).
|
||||
extract_to: <SD card>/
|
||||
# The boot logo MinUI writes to the m17's screen, rows stored bottom up.
|
||||
logo: https://raw.githubusercontent.com/shauninman/MinUI/7c8b818e869302780c4083b8a9d6309b6d4a9ccd/skeleton/EXTRAS/Tools/m17/Bootlogo.pak/logo.bmp
|
||||
logo_flip: true
|
||||
logo_trim: true
|
||||
scraper: minui
|
||||
# minarch hands each core a folder and names no file (workspace/all/minarch/minarch.c:2943):
|
||||
# there is no BIOS list of its own to export corrected.
|
||||
@@ -1124,6 +1128,9 @@ platforms:
|
||||
# minarch hands each core SDCARD_PATH/Bios/<TAG>
|
||||
# (workspace/all/minarch/minarch.c:3532).
|
||||
extract_to: <SD card>/
|
||||
# The boot screen MyMinUI shows on H700 handhelds.
|
||||
logo: https://raw.githubusercontent.com/Turro75/MyMinUI/d5ab4f9f83280f4316cb070c529299cb81ccc473/workspace/h700/boot/bootlogo.png
|
||||
logo_trim: true
|
||||
scraper: myminui
|
||||
# minarch hands each core a folder and names no file (workspace/all/minarch/minarch.c:3532):
|
||||
# there is no BIOS list of its own to export corrected.
|
||||
|
||||
@@ -52,14 +52,33 @@ def fetch(url: str) -> bytes:
|
||||
return response.read()
|
||||
|
||||
|
||||
def square_image(data: bytes, size: int, kind: str = "PNG") -> bytes:
|
||||
def square_image(data: bytes, size: int, kind: str = "PNG", flip: bool = False,
|
||||
trim: bool = False) -> bytes:
|
||||
"""*flip* mirrors the image top to bottom, for a boot logo stored in the
|
||||
row order a device's screen reads (MinUI's). *trim* cuts the plain
|
||||
background around the mark, the corner pixel's colour, and keeps that
|
||||
colour around it: a 640 x 480 boot screen became a dot."""
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import Image, ImageChops
|
||||
except ImportError as exc:
|
||||
raise SystemExit("Pillow is needed to normalise raster images: pip install Pillow") from exc
|
||||
image = Image.open(io.BytesIO(data)).convert("RGBA")
|
||||
if flip:
|
||||
image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
||||
background = (0, 0, 0, 0)
|
||||
if trim:
|
||||
background = image.getpixel((0, 0))
|
||||
# Faint noise in the background is no part of the mark.
|
||||
box = (ImageChops.difference(image, Image.new("RGBA", image.size, background))
|
||||
.convert("L").point(lambda value: 255 if value > 48 else 0).getbbox())
|
||||
if box:
|
||||
image = image.crop(box)
|
||||
side = round(max(image.size) * 1.2)
|
||||
framed = Image.new("RGBA", (side, side), background)
|
||||
framed.paste(image, ((side - image.width) // 2, (side - image.height) // 2))
|
||||
image = framed
|
||||
image.thumbnail((size, size), Image.LANCZOS)
|
||||
tile = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
tile = Image.new("RGBA", (size, size), background)
|
||||
tile.paste(image, ((size - image.width) // 2, (size - image.height) // 2), image)
|
||||
out = io.BytesIO()
|
||||
if kind == "WEBP":
|
||||
@@ -130,14 +149,19 @@ def write_manifest(directory: Path, note: str, entries: list[dict]) -> None:
|
||||
|
||||
|
||||
def fetch_logos() -> None:
|
||||
from siteguides import GUIDES
|
||||
from siteguides import GROUP_TILES, 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)
|
||||
entries = []
|
||||
for name, entry in sorted(platforms.items()):
|
||||
url = entry.get("logo")
|
||||
# The tiles of a kind of device (Android handhelds) beside the setups
|
||||
# carry a logo too, named after the tile.
|
||||
sources = [(name, entry.get("logo"), entry, GUIDES.get(name))
|
||||
for name, entry in sorted(platforms.items())]
|
||||
sources += [(re.sub(r"[^a-z0-9]+", "-", tile.name.lower()).strip("-"), tile.logo, {}, tile)
|
||||
for tiles in GROUP_TILES.values() for tile in tiles]
|
||||
for name, url, entry, guide in sources:
|
||||
if not url:
|
||||
# No logo of its own: the tile shows initials, and an old copy
|
||||
# would still be served.
|
||||
@@ -149,13 +173,14 @@ def fetch_logos() -> None:
|
||||
if is_svg(data):
|
||||
file_name, payload = f"{name}.svg", minify_svg(shrink_embedded(safe_svg(data, url)))
|
||||
else:
|
||||
file_name, payload = f"{name}.webp", square_image(data, LOGO_SIZE, "WEBP")
|
||||
file_name, payload = f"{name}.webp", square_image(
|
||||
data, LOGO_SIZE, "WEBP", flip=bool(entry.get("logo_flip")),
|
||||
trim=bool(entry.get("logo_trim")))
|
||||
for stale in LOGO_DIR.glob(f"{name}.*"):
|
||||
if stale.name != file_name:
|
||||
stale.unlink()
|
||||
(LOGO_DIR / file_name).write_bytes(payload)
|
||||
# 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({
|
||||
|
||||
@@ -586,7 +586,8 @@ def sample_writers(box: Sandbox) -> dict[str, Callable[[str], str]]:
|
||||
f"{list_date('retroarch')}:\n\n"
|
||||
+ _fenced([_bulk_line(box)])),
|
||||
"run-check": lambda _body: _fenced(
|
||||
_lines_from(check_run(), "Checking existing files", " Everything is in place")),
|
||||
_lines_from(check_run(), "Checking existing files",
|
||||
" Every file on the list is in place")),
|
||||
"check-refused": lambda _body: _fenced(_refused_lines(box.small("refused"))),
|
||||
"from-run": lambda _body: _fenced(
|
||||
_lines_from(box.small("from"), "Done.", count=2)),
|
||||
|
||||
@@ -441,7 +441,8 @@ def _tiles(registry: dict, members: list[str], display: dict[str, str], prefix:
|
||||
str(registry.get(name, {}).get("logo") or ""),
|
||||
GUIDES[name].runs_on or _runs_on(registry.get(name, {}), GUIDES[name]))
|
||||
for name in members),
|
||||
*((tile.name, f"{prefix}{tile.link}", "", tile.text) for tile in GROUP_TILES.get(group, ())),
|
||||
*((tile.name, f"{prefix}{tile.link}", tile.logo, tile.text)
|
||||
for tile in GROUP_TILES.get(group, ())),
|
||||
], eager=eager)
|
||||
|
||||
|
||||
@@ -506,7 +507,7 @@ def generate_home(
|
||||
"## Is it needed? { #do-i-need-it }",
|
||||
"",
|
||||
"- **Yes**, if the emulator says a BIOS is missing.",
|
||||
"- **Yes** for PlayStation 1 and 2, Saturn and Neo Geo.",
|
||||
"- **Yes** for PlayStation 2 and Neo Geo.",
|
||||
"- **Better with it** for Dreamcast: some games need it.",
|
||||
"- **Usually not** for NES, Super Nintendo, Mega Drive and Game Boy.",
|
||||
"",
|
||||
@@ -4201,7 +4202,7 @@ def check_pass_line() -> str:
|
||||
no file is missing or wrong (install.py EVERYTHING_IN_PLACE), read from
|
||||
the installer. Typed here, it kept "All files up to date." after the
|
||||
installer said "Everything is in place."."""
|
||||
return f"All good when it says `{_installer().EVERYTHING_IN_PLACE}`"
|
||||
return f"Good result: `{_installer().EVERYTHING_IN_PLACE}`"
|
||||
|
||||
# Under "From your computer": a device with no network installs from the ZIP
|
||||
# (`--from-pack`). The id is the one wiki/installer.md gives its section.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The pages a deploy changes, as the IndexNow list a search engine takes.
|
||||
|
||||
A search engine revisits a page on its own schedule, and an assistant that
|
||||
searches through it reads the copy it last saw. IndexNow (Bing, Yandex,
|
||||
Seznam, Naver) takes a list of changed addresses and fetches them soon
|
||||
after. Google does not take it and reads the sitemap.
|
||||
|
||||
The built site carries `digests.json`, each page's SHA-256 before its build
|
||||
line (scripts/site_markdown.py). This compares it with the one the live site
|
||||
serves, before the deploy replaces it: a page whose digest differs, a new
|
||||
page and a removed one are listed. The deploy job posts the list once the
|
||||
new site is live (.github/workflows/deploy-site.yml).
|
||||
|
||||
python scripts/indexnow.py --dist website/dist # print the list
|
||||
python scripts/indexnow.py --dist website/dist --output "$GITHUB_OUTPUT"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from siteagents import INDEXNOW_FILE, INDEXNOW_KEY, SITE # noqa: E402
|
||||
|
||||
HOST = "abdess.github.io"
|
||||
# What one IndexNow request accepts.
|
||||
MAX_URLS = 10_000
|
||||
USER_AGENT = "retrobios-indexnow"
|
||||
|
||||
|
||||
def live_digests(url: str = f"{SITE}digests.json") -> dict[str, str]:
|
||||
"""The digests the live site serves. None yet (the first deploy that
|
||||
writes them) is an empty map: every page is new."""
|
||||
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=60) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
return {}
|
||||
raise
|
||||
|
||||
|
||||
def changed_urls(new: dict[str, str], old: dict[str, str]) -> list[str]:
|
||||
"""Pages added, changed or removed between two digest maps."""
|
||||
return sorted(url for url in new.keys() | old.keys() if new.get(url) != old.get(url))
|
||||
|
||||
|
||||
def payload(urls: list[str]) -> dict:
|
||||
if len(urls) > MAX_URLS:
|
||||
raise SystemExit(f"error: {len(urls)} changed pages, one request takes {MAX_URLS}")
|
||||
return {"host": HOST, "key": INDEXNOW_KEY, "keyLocation": f"{SITE}{INDEXNOW_FILE}",
|
||||
"urlList": urls}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], allow_abbrev=False)
|
||||
parser.add_argument("--dist", type=Path, required=True, help="the built site (website/dist)")
|
||||
parser.add_argument("--output", type=Path,
|
||||
help="append payload=<json> to this file (a GitHub step output)")
|
||||
args = parser.parse_args()
|
||||
new = json.loads((args.dist / "digests.json").read_text(encoding="utf-8"))
|
||||
urls = changed_urls(new, live_digests())
|
||||
print(f"{len(urls)} of {len(new)} pages changed", file=sys.stderr)
|
||||
if not urls:
|
||||
return 0
|
||||
body = json.dumps(payload(urls), separators=(",", ":"))
|
||||
if args.output:
|
||||
with args.output.open("a", encoding="utf-8") as handle:
|
||||
handle.write(f"payload={body}\n")
|
||||
else:
|
||||
print(body)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+64
-10
@@ -11,6 +11,13 @@ website/astro.config.mjs), and llms.txt says the convention.
|
||||
`llms-full.txt` joins the Markdown of the pages a setup is done with:
|
||||
the start pages, the guides and each setup's page.
|
||||
|
||||
An assistant often reads a copy fetched long before, by itself or by a
|
||||
search engine. Each copy, llms.txt and llms-full.txt name the commit and
|
||||
the date they were built from, and `version.json` names the current one,
|
||||
so a stale copy can be told apart. `digests.json` gives each page's
|
||||
SHA-256 before that line: the next deploy compares it with its own and
|
||||
tells search engines which pages changed (scripts/indexnow.py).
|
||||
|
||||
Run by the Astro build once the pages are written (astro.config.mjs),
|
||||
on the built site:
|
||||
|
||||
@@ -20,8 +27,11 @@ on the built site:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
@@ -293,42 +303,86 @@ def page_markdown(html: str, page_url: str) -> str:
|
||||
return converter.markdown()
|
||||
|
||||
|
||||
def convert_site(dist: Path) -> int:
|
||||
"""Write index.md beside every page's index.html, then llms-full.txt."""
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def build_stamp() -> dict[str, str]:
|
||||
"""The commit the site is built from and its date. CI names the commit
|
||||
in GITHUB_SHA. The date is the commit's, so two builds of one commit
|
||||
write the same bytes."""
|
||||
commit = os.environ.get("GITHUB_SHA") or subprocess.run(
|
||||
["git", "rev-parse", "HEAD"], cwd=ROOT, capture_output=True, text=True,
|
||||
check=True).stdout.strip()
|
||||
date = subprocess.run(["git", "log", "-1", "--format=%cI", commit], cwd=ROOT,
|
||||
capture_output=True, text=True, check=True).stdout.strip()
|
||||
return {"commit": commit, "date": date}
|
||||
|
||||
|
||||
def stamp_line(stamp: dict[str, str]) -> str:
|
||||
return (f"> Built from commit {stamp['commit'][:12]} on {stamp['date'][:10]}. "
|
||||
f"The current build: {SITE}{BASE}version.json")
|
||||
|
||||
|
||||
def stamped(text: str, stamp: dict[str, str]) -> str:
|
||||
"""*text* with the build line under its first line, the title."""
|
||||
first, _, rest = text.partition("\n")
|
||||
return f"{first}\n\n{stamp_line(stamp)}\n{rest}"
|
||||
|
||||
|
||||
def convert_site(dist: Path, stamp: dict[str, str] | None = None) -> int:
|
||||
"""Write index.md beside every page's index.html, then llms-full.txt,
|
||||
version.json and digests.json, and stamp llms.txt."""
|
||||
stamp = stamp or build_stamp()
|
||||
written = 0
|
||||
digests: dict[str, str] = {}
|
||||
for html_path in sorted(dist.rglob("index.html")):
|
||||
relative = html_path.parent.relative_to(dist).as_posix()
|
||||
page = "" if relative == "." else f"{relative}/"
|
||||
url = f"{SITE}{BASE}{page}"
|
||||
text = page_markdown(html_path.read_text(encoding="utf-8"), url)
|
||||
(html_path.parent / "index.md").write_text(text, encoding="utf-8")
|
||||
digests[url] = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
(html_path.parent / "index.md").write_text(stamped(text, stamp), encoding="utf-8")
|
||||
written += 1
|
||||
write_full(dist)
|
||||
write_full(dist, stamp)
|
||||
llms = dist / "llms.txt"
|
||||
if llms.is_file():
|
||||
text = llms.read_text(encoding="utf-8")
|
||||
digests[f"{SITE}{BASE}llms.txt"] = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
# Under the summary, which llms.txt keeps as the quote after its title.
|
||||
title, summary, rest = text.split("\n\n", 2)
|
||||
line = stamp_line(stamp).removeprefix("> ")
|
||||
llms.write_text(f"{title}\n\n{summary}\n\n{line}\n\n{rest}", encoding="utf-8")
|
||||
(dist / "version.json").write_text(json.dumps(stamp, indent=1) + "\n", encoding="utf-8")
|
||||
(dist / "digests.json").write_text(
|
||||
json.dumps(dict(sorted(digests.items())), indent=0) + "\n", encoding="utf-8")
|
||||
return written
|
||||
|
||||
|
||||
def write_full(dist: Path) -> None:
|
||||
def _unstamped(text: str, stamp: dict[str, str]) -> str:
|
||||
"""A page's copy without its build line: llms-full.txt says it once."""
|
||||
return text.replace(f"\n\n{stamp_line(stamp)}\n", "\n", 1)
|
||||
|
||||
|
||||
def write_full(dist: Path, stamp: dict[str, str]) -> None:
|
||||
"""llms-full.txt from the pages' Markdown copies."""
|
||||
parts = ["# RetroBIOS: the guides in one file", "",
|
||||
parts = ["# RetroBIOS: the guides in one file", "", stamp_line(stamp), "",
|
||||
"> The pages a setup is done with, in reading order. Every page of the site also has "
|
||||
"its own Markdown copy at `<page>/index.md`.", ""]
|
||||
for page in FULL_PAGES:
|
||||
md = dist / page / "index.md"
|
||||
if md.is_file():
|
||||
parts += [md.read_text(encoding="utf-8").rstrip(), "", "---", ""]
|
||||
parts += [_unstamped(md.read_text(encoding="utf-8"), stamp).rstrip(), "", "---", ""]
|
||||
# Each setup's own page (the setups the picker lists, not the parts a
|
||||
# long page is cut into), up to its reference of every file: what to
|
||||
# do and where, not the full lists, which its own copy keeps. All of
|
||||
# them was 6.9 MB, past what an assistant reads at once.
|
||||
import json # noqa: PLC0415
|
||||
|
||||
index = dist / "data" / "picker" / "index.json"
|
||||
setups = [str(item["platform"]) for item in json.loads(index.read_text(encoding="utf-8"))] \
|
||||
if index.is_file() else []
|
||||
for setup in [*setups, "linux-handhelds"]:
|
||||
md = dist / "platforms" / setup / "index.md"
|
||||
if md.is_file():
|
||||
text = md.read_text(encoding="utf-8")
|
||||
text = _unstamped(md.read_text(encoding="utf-8"), stamp)
|
||||
parts += [text.split("\n## Reference", 1)[0].rstrip(), "", "---", ""]
|
||||
(dist / "llms-full.txt").write_text("\n".join(parts).rstrip() + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
+12
-2
@@ -73,8 +73,8 @@ def llms_txt(platforms: list[tuple[str, str]]) -> str:
|
||||
"code: which files an emulator reads, whether it starts without them, where each one "
|
||||
"goes on every setup, their hashes, and an installer that puts them in place.",
|
||||
"",
|
||||
"Every claim about a file cites the line of emulator source code that reads it. The "
|
||||
"files themselves are served by the GitHub repository "
|
||||
"Each emulator's files are read from its code, and most cite the file and line that "
|
||||
"read them. The files themselves are served by the GitHub repository "
|
||||
"(https://github.com/Abdess/retrobios) and its releases.",
|
||||
"",
|
||||
"## Answering a user",
|
||||
@@ -131,6 +131,9 @@ def llms_txt(platforms: list[tuple[str, str]]) -> str:
|
||||
# folders and hashes change with each setup, and a guessed one is the
|
||||
# answer that fails. The same rules end the skill.
|
||||
ANSWER_RULES = (
|
||||
"Each Markdown copy, llms.txt and llms-full.txt name the commit and the date they "
|
||||
f"were built from. When a copy read earlier or found through a search engine names a "
|
||||
f"commit other than {SITE}version.json, fetch it again: the site changes with each commit.",
|
||||
"Answer from the pages and data, not from memory. Every page has a Markdown copy: add "
|
||||
"`index.md` to its address (https://abdess.github.io/retrobios/which-pack/index.md). "
|
||||
f"[llms-full.txt]({SITE}llms-full.txt) holds the guides and every setup's page in one file.",
|
||||
@@ -247,9 +250,16 @@ def ard_manifest() -> dict:
|
||||
}
|
||||
|
||||
|
||||
# IndexNow: the key search engines read back from the site before they take
|
||||
# a list of changed pages from it (scripts/indexnow.py). Public by design.
|
||||
INDEXNOW_KEY = "23329654933fb67280e8e7ac749eb869"
|
||||
INDEXNOW_FILE = "indexnow.txt"
|
||||
|
||||
|
||||
def write(public: Path, platforms: list[tuple[str, str]],
|
||||
write_if_changed: Callable[[str, str], object]) -> None:
|
||||
write_if_changed(str(public / "llms.txt"), llms_txt(platforms))
|
||||
write_if_changed(str(public / INDEXNOW_FILE), INDEXNOW_KEY + "\n")
|
||||
(public / SKILL_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
write_if_changed(str(public / SKILL_PATH), skill_md())
|
||||
write_if_changed(str(public / "ard.json"), json.dumps(ard_manifest(), indent=1) + "\n")
|
||||
+11
-2
@@ -81,6 +81,10 @@ class Tile:
|
||||
name: str
|
||||
text: str
|
||||
link: str
|
||||
# A logo naming the kind of device, credited like a setup's.
|
||||
logo: str = ""
|
||||
logo_owner: str = ""
|
||||
logo_licence: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -129,6 +133,7 @@ _TOP_KEYS = {"install_base", "trust_items", "device_groups", "terminals", "guide
|
||||
_GROUP_KEYS = {"label", "description", "anchor", "also_runs", "tiles"}
|
||||
_ALSO_KEYS = ("setup", "summary", "uses", "runs_on", "folder")
|
||||
_TILE_KEYS = ("name", "text", "link")
|
||||
_TILE_LOGO_KEYS = ("logo", "logo_owner", "logo_licence")
|
||||
_HANDHELD_KEYS = {"page", "title", "summary", "uses", "systems"}
|
||||
_SYSTEM_KEYS = {"name", "folder", "folder_source", "devices", "devices_source", "linux_only"}
|
||||
_SOURCE = re.compile(r"https://\S+")
|
||||
@@ -342,8 +347,12 @@ def load_guides(path: Path = GUIDES_FILE, registry: dict | None = None) -> dict:
|
||||
row_at = f"{at}.tiles[{i}]"
|
||||
if not isinstance(row, dict):
|
||||
raise GuideError(f"{row_at}: expected a mapping")
|
||||
_unknown(row, _TILE_KEYS, row_at)
|
||||
group_tiles.append(Tile(*(_text(row.get(k), f"{row_at}.{k}") for k in _TILE_KEYS)))
|
||||
_unknown(row, _TILE_KEYS + _TILE_LOGO_KEYS, row_at)
|
||||
if row.get("logo") and not all(row.get(k) for k in _TILE_LOGO_KEYS):
|
||||
raise GuideError(f"{row_at}: a logo needs logo_owner and logo_licence")
|
||||
group_tiles.append(Tile(
|
||||
*(_text(row.get(k), f"{row_at}.{k}") for k in _TILE_KEYS),
|
||||
*(_text(row[k], f"{row_at}.{k}") if row.get(k) else "" for k in _TILE_LOGO_KEYS)))
|
||||
if group_tiles:
|
||||
tiles[key] = tuple(group_tiles)
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -370,7 +370,7 @@
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
Vendored
+1
-1
@@ -118,7 +118,7 @@
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
|
||||
BIOS folder:
|
||||
^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
|
||||
Vendored
+1
-1
@@ -145,7 +145,7 @@
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
|
||||
BIOS folder:
|
||||
^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
|
||||
Vendored
+1
-1
@@ -226,7 +226,7 @@
|
||||
=== plan-in-place
|
||||
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
|
||||
|
||||
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
|
||||
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
|
||||
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
|
||||
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
Vendored
+3
-3
@@ -458,7 +458,7 @@
|
||||
=== plan-in-place 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -471,7 +471,7 @@
|
||||
=== plan-in-place 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -487,7 +487,7 @@
|
||||
=== plan-in-place 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
+3
-3
@@ -458,7 +458,7 @@
|
||||
=== plan-in-place 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -471,7 +471,7 @@
|
||||
=== plan-in-place 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -487,7 +487,7 @@
|
||||
=== plan-in-place 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
+3
-3
@@ -458,7 +458,7 @@
|
||||
=== plan-in-place 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -471,7 +471,7 @@
|
||||
=== plan-in-place 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -487,7 +487,7 @@
|
||||
=== plan-in-place 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
Vendored
+3
-3
@@ -433,7 +433,7 @@
|
||||
=== plan-in-place 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -446,7 +446,7 @@
|
||||
=== plan-in-place 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -462,7 +462,7 @@
|
||||
=== plan-in-place 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
Vendored
+3
-3
@@ -487,7 +487,7 @@
|
||||
=== plan-in-place 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -500,7 +500,7 @@
|
||||
=== plan-in-place 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -516,7 +516,7 @@
|
||||
=== plan-in-place 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
Vendored
+3
-3
@@ -487,7 +487,7 @@
|
||||
=== plan-in-place 40x12
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -500,7 +500,7 @@
|
||||
=== plan-in-place 40x15
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder:
|
||||
~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
@@ -516,7 +516,7 @@
|
||||
=== plan-in-place 80x24
|
||||
RetroBIOS: RetroArch
|
||||
|
||||
Everything is in place
|
||||
Every file on the list is in place
|
||||
BIOS folder: ~/.config/retroarch/system
|
||||
5 in place, 0 missing, 0 wrong copies
|
||||
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
|
||||
|
||||
@@ -1665,7 +1665,7 @@ class InstallerPagesFollowTheCode(unittest.TestCase):
|
||||
|
||||
def test_a_message_the_installer_does_not_print_fails(self):
|
||||
corpus = _literals()
|
||||
self.assertEqual(unprinted_messages("It ends `Everything is in place.` here.\n", corpus), [])
|
||||
self.assertEqual(unprinted_messages("It ends `Every file on the list is in place.` here.\n", corpus), [])
|
||||
self.assertEqual(len(unprinted_messages("It says `All BIOS files are present.`\n", corpus)), 1)
|
||||
self.assertEqual(unprinted_messages(
|
||||
"`E:\\ holds 38 of these files: a run without --check copies them.`\n", corpus), [])
|
||||
|
||||
@@ -5674,7 +5674,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
|
||||
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest),
|
||||
"--system", "sony-psp", "--check", "--no-input")
|
||||
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
|
||||
self.assertNotIn("Everything is in place", proc.stdout)
|
||||
self.assertNotIn("Every file on the list is in place", proc.stdout)
|
||||
found = re.search(r"(\d+) in place, (\d+) missing", proc.stdout)
|
||||
self.assertIsNotNone(found, proc.stdout)
|
||||
self.assertIn(f"\n {found.group(2)} data files to fetch\n", proc.stdout)
|
||||
@@ -5949,7 +5949,7 @@ class StandaloneEmulatorsWithoutAPlatformAreServed(unittest.TestCase, _LoopbackR
|
||||
if name not in named and not name.startswith("scph")], [])
|
||||
proc = _install_run(self.env, "--no-input", "--check")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn("Everything is in place.", proc.stdout)
|
||||
self.assertIn("Every file on the list is in place.", proc.stdout)
|
||||
|
||||
def test_no_emulator_folder_keeps_the_hint(self):
|
||||
self.duckstation.rmdir()
|
||||
|
||||
@@ -13230,7 +13230,7 @@ class OneVocabularyOneCount(unittest.TestCase):
|
||||
with self.subTest(**given):
|
||||
text = self.run_line(listing, dest, [""], offline=True, **given)
|
||||
self.assertIn("2 in place, 0 missing, 0 wrong copies", text)
|
||||
self.assertIn("Everything is in place", text)
|
||||
self.assertIn("Every file on the list is in place", text)
|
||||
self.assertNotIn("up to date", text)
|
||||
self.assertNotIn("0 files installed", text)
|
||||
state = checked_state()
|
||||
@@ -13239,7 +13239,7 @@ class OneVocabularyOneCount(unittest.TestCase):
|
||||
"failed": []}
|
||||
state.code = 0
|
||||
_title, lines = install.tui_outcome(state)
|
||||
self.assertEqual(lines[0], "Everything is in place.")
|
||||
self.assertEqual(lines[0], "Every file on the list is in place.")
|
||||
|
||||
def test_check_names_a_wrong_copy_as_such(self):
|
||||
listing, dest, _old = self.two_files()
|
||||
|
||||
@@ -324,9 +324,17 @@ class TitleLogoOnAPlate(unittest.TestCase):
|
||||
self.assertIn('height="40"', image)
|
||||
|
||||
def test_a_setup_with_no_logo_gets_its_initials(self):
|
||||
# Every setup has a logo today: the fallback is read where it is
|
||||
# written, the tile's and the title's.
|
||||
import sitemarkup # noqa: PLC0415
|
||||
|
||||
self.assertRegex(sitemarkup._tile_mark("MyMinUI", ""), r"<svg[^>]*>.*>MM</text></svg>")
|
||||
component = _read(REPO_ROOT / "website" / "src" / "components" / "PageTitle.astro")
|
||||
self.assertIn("{initials(setup.name)}", component)
|
||||
|
||||
def test_myminui_shows_its_logo(self):
|
||||
block = _title_through_heading(_built("platforms/myminui"))
|
||||
self.assertNotIn("<img", block)
|
||||
self.assertRegex(block, r"<svg class=\"rb-title-mark[^>]*>.*?>MM</text>")
|
||||
self.assertIn('src="/retrobios/assets/logos/myminui.webp"', block)
|
||||
|
||||
|
||||
class NoLevelBadgeOnATabRoot(unittest.TestCase):
|
||||
@@ -616,7 +624,7 @@ class LogosAtTheirDisplaySize(unittest.TestCase):
|
||||
self.assertEqual(sorted(p.name for p in LOGOS.glob("*.png")), [])
|
||||
script = _read(REPO_ROOT / "scripts" / "fetch_site_images.py")
|
||||
self.assertIn(f"LOGO_SIZE = {LOGO_PIXELS}", script)
|
||||
self.assertIn('square_image(data, LOGO_SIZE, "WEBP")', script)
|
||||
self.assertIn('square_image(\n data, LOGO_SIZE, "WEBP"', script)
|
||||
|
||||
def test_pages_point_at_the_copies(self):
|
||||
html = _built("platforms/emudeck")
|
||||
|
||||
@@ -607,7 +607,7 @@ class YourSetupIndexInPlainWords(unittest.TestCase):
|
||||
class HomeCopy(unittest.TestCase):
|
||||
def test_dreamcast_and_the_check_link(self):
|
||||
home = re.sub(r"</?abbr[^>]*>", "", _page("index.md"))
|
||||
self.assertIn("- **Yes** for PlayStation 1 and 2, Saturn and Neo Geo.", home)
|
||||
self.assertIn("- **Yes** for PlayStation 2 and Neo Geo.", home)
|
||||
self.assertIn("- **Better with it** for Dreamcast: some games need it.", home)
|
||||
# A question and the link that answers it: no level label, which
|
||||
# read as a broken crumb beside the Reference tab.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""A copy an assistant read long ago can be told from the current one.
|
||||
|
||||
Each Markdown copy, llms.txt and llms-full.txt name the commit they were
|
||||
built from, version.json names the current one, and the next deploy lists
|
||||
the pages whose words changed for IndexNow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import tests # noqa: F401
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import indexnow # noqa: E402
|
||||
import siteagents # noqa: E402
|
||||
import site_markdown # noqa: E402
|
||||
|
||||
STAMP = {"commit": "0123456789abcdef0123456789abcdef01234567", "date": "2026-11-09T08:00:00+01:00"}
|
||||
# The two parts a built Starlight page gives the copy: its title and its content.
|
||||
PAGE = ('<html><body><h1 id="_top">Title</h1>'
|
||||
'<div class="sl-markdown-content"><p>Words of the page.</p></div></body></html>')
|
||||
|
||||
|
||||
def _site(root: Path, words: str = "Words of the page.") -> Path:
|
||||
dist = root / "dist"
|
||||
for page in ("", "wiki/faq/"):
|
||||
(dist / page).mkdir(parents=True, exist_ok=True)
|
||||
(dist / page / "index.html").write_text(PAGE.replace("Words of the page.", words),
|
||||
encoding="utf-8")
|
||||
(dist / "llms.txt").write_text("# RetroBIOS\n\n> The summary.\n\n## Start\n", encoding="utf-8")
|
||||
return dist
|
||||
|
||||
|
||||
class BuildStamp(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory(dir=ROOT / "tmp")
|
||||
self.dist = _site(Path(self.tmp.name))
|
||||
site_markdown.convert_site(self.dist, STAMP)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_each_copy_names_its_commit_under_its_title(self):
|
||||
lines = (self.dist / "wiki" / "faq" / "index.md").read_text(encoding="utf-8").splitlines()
|
||||
self.assertTrue(lines[0].startswith("# "))
|
||||
self.assertEqual(lines[2], "> Built from commit 0123456789ab on 2026-11-09. "
|
||||
"The current build: https://abdess.github.io/retrobios/version.json")
|
||||
|
||||
def test_version_json_names_the_build(self):
|
||||
self.assertEqual(json.loads((self.dist / "version.json").read_text(encoding="utf-8")), STAMP)
|
||||
|
||||
def test_llms_txt_keeps_its_summary_after_the_title(self):
|
||||
text = (self.dist / "llms.txt").read_text(encoding="utf-8")
|
||||
self.assertTrue(text.startswith("# RetroBIOS\n\n> The summary.\n\nBuilt from commit 0123456789ab"))
|
||||
|
||||
def test_llms_full_says_the_build_once(self):
|
||||
text = (self.dist / "llms-full.txt").read_text(encoding="utf-8")
|
||||
self.assertEqual(text.count("Built from commit"), 1)
|
||||
|
||||
def test_digests_ignore_the_build_line(self):
|
||||
first = json.loads((self.dist / "digests.json").read_text(encoding="utf-8"))
|
||||
with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as other:
|
||||
dist = _site(Path(other))
|
||||
site_markdown.convert_site(dist, {"commit": "f" * 40, "date": "2026-11-10T00:00:00Z"})
|
||||
second = json.loads((dist / "digests.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(first, second)
|
||||
self.assertIn("https://abdess.github.io/retrobios/llms.txt", first)
|
||||
|
||||
|
||||
class IndexNowList(unittest.TestCase):
|
||||
def test_added_changed_and_removed_pages_are_listed(self):
|
||||
old = {"a": "1", "b": "2", "gone": "3"}
|
||||
new = {"a": "1", "b": "9", "new": "4"}
|
||||
self.assertEqual(indexnow.changed_urls(new, old), ["b", "gone", "new"])
|
||||
|
||||
def test_the_first_deploy_lists_every_page(self):
|
||||
self.assertEqual(indexnow.changed_urls({"a": "1", "b": "2"}, {}), ["a", "b"])
|
||||
|
||||
def test_the_key_is_served_where_the_request_says(self):
|
||||
body = indexnow.payload(["https://abdess.github.io/retrobios/"])
|
||||
self.assertEqual(body["keyLocation"], "https://abdess.github.io/retrobios/indexnow.txt")
|
||||
with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as public:
|
||||
written: dict[str, str] = {}
|
||||
siteagents.write(Path(public), [], lambda path, text: written.__setitem__(path, text))
|
||||
self.assertEqual(written[str(Path(public) / "indexnow.txt")].strip(), body["key"])
|
||||
|
||||
def test_the_deploy_posts_the_list_after_the_site_is_live(self):
|
||||
workflow = (ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(encoding="utf-8")
|
||||
listed = workflow.index("scripts/indexnow.py --dist website/dist")
|
||||
deployed = workflow.index("actions/deploy-pages@")
|
||||
posted = workflow.index("https://api.indexnow.org/indexnow")
|
||||
self.assertLess(listed, deployed)
|
||||
self.assertLess(deployed, posted)
|
||||
|
||||
|
||||
class AnswerRule(unittest.TestCase):
|
||||
def test_assistants_are_told_to_compare_the_commit(self):
|
||||
self.assertTrue(any("version.json" in rule for rule in siteagents.ANSWER_RULES))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -82,15 +82,22 @@ class PlatformLogos(unittest.TestCase):
|
||||
self.assertTrue(self.registry["bizhawk"]["logo"].endswith(
|
||||
"/src/BizHawk.Client.EmuHawk/images/logo.ico"))
|
||||
|
||||
def test_minui_has_no_logo_and_no_stale_copy(self):
|
||||
self.assertNotIn("logo", self.registry["minui"])
|
||||
self.assertEqual(list((REPO_ROOT / "docs_assets" / "logos").glob("minui.*")), [])
|
||||
def test_minui_shows_its_boot_logo_not_its_author(self):
|
||||
# The author's GitHub avatar is a face, not the project's mark.
|
||||
self.assertIn("/Bootlogo.pak/", self.registry["minui"]["logo"])
|
||||
self.assertTrue(self.registry["minui"]["logo_flip"])
|
||||
|
||||
def test_every_logo_copy_has_a_registry_logo(self):
|
||||
def test_every_logo_copy_has_a_registry_or_tile_logo(self):
|
||||
from siteguides import GROUP_TILES # noqa: PLC0415
|
||||
|
||||
tiles = {re.sub(r"[^a-z0-9]+", "-", tile.name.lower()).strip("-"): tile.logo
|
||||
for group in GROUP_TILES.values() for tile in group}
|
||||
sources = json.loads((REPO_ROOT / "docs_assets" / "logos" / "sources.json").read_text())
|
||||
for entry in sources["images"]:
|
||||
with self.subTest(platform=entry["platform"]):
|
||||
self.assertEqual(self.registry[entry["platform"]].get("logo"), entry["source"])
|
||||
logo = (self.registry[entry["platform"]].get("logo")
|
||||
if entry["platform"] in self.registry else tiles.get(entry["platform"]))
|
||||
self.assertEqual(logo, entry["source"])
|
||||
|
||||
|
||||
class NodeCompileCache(unittest.TestCase):
|
||||
|
||||
@@ -295,6 +295,8 @@ that text belongs in a guide field.
|
||||
|
||||
```yaml
|
||||
logo: https://... # SVG or PNG for UI/docs
|
||||
logo_flip: true # a raster stored bottom row first (a boot logo)
|
||||
logo_trim: true # cut the plain background around the mark
|
||||
inherits_from: retroarch # the parent, for readers: the merge reads
|
||||
# inherits: in the platform YAML
|
||||
case_insensitive_fs: true # if the platform runs on case-insensitive filesystems
|
||||
|
||||
+10
-12
@@ -9,9 +9,8 @@ terms: ios, iphone, ipad
|
||||
## Are BIOS files needed? { #do-i-need-bios-files }
|
||||
|
||||
- **Yes** for PlayStation 2 and Neo Geo.
|
||||
- **Most emulators** for PlayStation 1 and Saturn. Some start games without
|
||||
it, with a built-in replacement.
|
||||
- **Better with it** for Dreamcast. Some games need it.
|
||||
- **Most emulators** for PlayStation 1 and Saturn.
|
||||
- **Better with it** for Dreamcast.
|
||||
- **Usually not** for NES or Super Nintendo.
|
||||
|
||||
[What is a BIOS?](what-are-bios-files.md)
|
||||
@@ -36,8 +35,8 @@ curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --system sony-playsta
|
||||
|
||||
## Does the BIOS set the game's language?
|
||||
|
||||
Rarely. It comes from the game. Some follow the console's setting, like Flycast's **Language**
|
||||
option.
|
||||
It mostly comes from the game. Some follow the console's setting, like Flycast's
|
||||
**Language** option.
|
||||
|
||||
## Zip or unzip? { #zip-or-unzip }
|
||||
|
||||
@@ -62,7 +61,7 @@ Game sets must match the emulator's version:
|
||||
|
||||
## Is the command safe? { #is-piping-the-installer-into-a-shell-safe }
|
||||
|
||||
- It stops if the installer does not match its expected fingerprint, and
|
||||
- It stops if the installer does not match its fingerprint, and
|
||||
checks each file.
|
||||
- It fills the BIOS folder, some save folders and a games cache.
|
||||
- It never touches games or save files.
|
||||
@@ -79,13 +78,13 @@ and download their files.
|
||||
|
||||
## Why is there no ZIP for a setup? { #why-is-there-no-zip-for-my-setup }
|
||||
|
||||
ZIPs are made for the setups most people download. The command installs every
|
||||
file the collection holds for each setup and checks each one. `--dry-run` shows the list first.
|
||||
ZIPs are made for the setups most people download. The command installs each
|
||||
setup's listed files and checks each one. `--dry-run` shows the list first.
|
||||
|
||||
## Fill an SD card from a computer?
|
||||
|
||||
The interface finds a muOS, Onion, spruceOS, MinUI, KNULLI, ROCKNIX, Lakka or
|
||||
MiSTer card. Else name the setup and its BIOS folder. For RetroArch on a Switch,
|
||||
MiSTer card. Else name its setup and BIOS folder. For RetroArch on a Switch,
|
||||
with the card in drive `E:`, run:
|
||||
|
||||
<!-- os-tabs windows: --platform retroarch --dest E:\retroarch\cores\system -->
|
||||
@@ -112,12 +111,11 @@ Fill it from a computer or point `--from-pack` at the ZIP: [Without a network](i
|
||||
|
||||
## Why is a file missing from the ZIP?
|
||||
|
||||
Some come with the emulator. Others are not in the collection yet:
|
||||
[what is still missing](../gaps.md) names each one.
|
||||
Some come with the emulator. Others are still missing.
|
||||
|
||||
## A game shows a black screen { #my-game-shows-a-black-screen }
|
||||
|
||||
Often a missing BIOS: [When a game does not start](troubleshooting.md).
|
||||
Maybe a missing BIOS: [When a game does not start](troubleshooting.md).
|
||||
|
||||
## Can an owned console's BIOS be copied? { #copy-your-own-bios }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ order: 30
|
||||
|
||||
# First install, step by step
|
||||
|
||||
From an empty BIOS folder to every BIOS file the setup's list names.
|
||||
From an empty BIOS folder to a checked one.
|
||||
|
||||
**Needed:** internet and some free space.
|
||||
|
||||
@@ -102,13 +102,12 @@ Esc or Ctrl+C stops, B on a handheld. Run again to resume.
|
||||
|
||||
## 5. Read the result
|
||||
|
||||
- **Done**: every file the collection holds for this setup is in place. The
|
||||
screen names any file it does not hold.
|
||||
- **Done**: every file on the list is in place.
|
||||
- **Finished with errors**: run it again. It retries only those.
|
||||
- **Finished with files missing**: run it again online.
|
||||
- **Nothing installed**: the screen says why. See [If it stops](#if-it-stops).
|
||||
|
||||
For line-by-line questions (screen readers), add `--plain`.
|
||||
Screen readers: add `--plain`.
|
||||
|
||||
## What the command does
|
||||
|
||||
@@ -123,16 +122,17 @@ Add `--check` to the same command. It changes nothing.
|
||||
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --check
|
||||
```
|
||||
|
||||
All good:
|
||||
Result:
|
||||
|
||||
```
|
||||
Everything is in place.
|
||||
Every file on the list is in place.
|
||||
```
|
||||
|
||||
Missing required files are named below it.
|
||||
|
||||
## Undo
|
||||
|
||||
Replaced files go to `retrobios-backup` beside the BIOS folder. The result
|
||||
says how to restore them.
|
||||
Replaced files go to `retrobios-backup`, and `--restore` puts them back.
|
||||
|
||||
## If it stops
|
||||
|
||||
|
||||
+2
-2
@@ -570,11 +570,11 @@ The same command with `--check` reads the folder and writes nothing:
|
||||
Checking existing files...
|
||||
18175 in place, 0 missing, 0 wrong copies
|
||||
|
||||
Everything is in place.
|
||||
Every file on the list is in place.
|
||||
```
|
||||
<!-- /generated: run-check -->
|
||||
|
||||
`Everything is in place.` is said only when the data folders are complete
|
||||
`Every file on the list is in place.` is said only when the data folders are complete
|
||||
too. With the BIOS files in place and data files missing (PPSSPP's assets,
|
||||
Dolphin's `Sys`), the line counts those instead (`169 data files to fetch`),
|
||||
and `--check` exits with code 3. The files the collection does not hold are
|
||||
|
||||
+2
-3
@@ -8,9 +8,8 @@ terms: license
|
||||
|
||||
It depends on the country. Nothing here is legal advice.
|
||||
|
||||
- US courts held that copying code to build a compatible emulator can be fair
|
||||
use (*Sega v. Accolade*, *Sony v. Connectix*). EU law allows it under
|
||||
conditions.
|
||||
- US courts have held that building an emulator can be fair use. EU law allows
|
||||
it under conditions.
|
||||
- Sharing BIOS files is not settled law. The strongest case is old consoles no
|
||||
longer sold. The weakest is encryption keys (Switch, 3DS, Wii U).
|
||||
- The files are here for personal backup and preservation.
|
||||
|
||||
@@ -53,10 +53,9 @@ file under the BIOS name: zeros, half the file, or another region's BIOS.
|
||||
|
||||
Each result is a full record in the
|
||||
[`measurements/`](https://github.com/Abdess/retrobios/tree/main/measurements)
|
||||
folder of this project. It names its method, the computer and the exact build
|
||||
of the emulator. Records are added, not edited. A change to an existing record (scratch paths
|
||||
removed on 2026-11-05 and 2026-11-06) is a commit of its own, visible in the
|
||||
folder's history.
|
||||
folder. It names its method, the computer and the exact build of the emulator.
|
||||
|
||||
Records are added. A change to one is a commit of its own.
|
||||
|
||||
The results so far ran on these computers:
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Pack probe | the cores a platform runs for a system, staging grown to a fixed point, runs mapped to pack and manifest entries, the line and its schema, the probe lock per run, that no page shows its lines. One class builds a real pack narrowed to PlayStation and lays it out both ways, from the archive and with `install.py --dest`, and skips without the collection | `test_pack_probe` |
|
||||
| Automation runner, disk cleanup, asset sync and bit-flip repair | fake jobs, git repositories with a bare origin and session worktrees, a committed file with one bit flipped, canned CI runs in place of `gh` | `test_autorun` |
|
||||
| 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_batocera_md5_list`, `test_check_freshness`, `test_cited_lines`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `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_optional_dependencies`, `test_pin_build`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_retired_files`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_agents`, `test_site_arcade_boards`, `test_site_availability`, `test_site_bootstrap`, `test_site_built_gate`, `test_site_components`, `test_site_console_folders`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_css`, `test_site_demo`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_emulators`, `test_site_entry`, `test_site_entry_tables`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_inputs`, `test_site_insights`, `test_site_integration`, `test_site_nav`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_privacy`, `test_site_pronouns`, `test_site_reference`, `test_site_render`, `test_site_search`, `test_site_steps`, `test_site_system_links`, `test_site_system_names`, `test_site_system_sections`, `test_site_systems`, `test_site_validation`, `test_site_weight`, `test_site_wiki`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_version_basis`, `test_workflow_paths` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_cited_lines`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `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_optional_dependencies`, `test_pin_build`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_retired_files`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_agents`, `test_site_arcade_boards`, `test_site_availability`, `test_site_bootstrap`, `test_site_built_gate`, `test_site_components`, `test_site_console_folders`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_css`, `test_site_demo`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_emulators`, `test_site_entry`, `test_site_entry_tables`, `test_site_exports`, `test_site_freshness`, `test_site_funnel`, `test_site_images`, `test_site_inputs`, `test_site_insights`, `test_site_integration`, `test_site_nav`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_privacy`, `test_site_pronouns`, `test_site_reference`, `test_site_render`, `test_site_search`, `test_site_steps`, `test_site_system_links`, `test_site_system_names`, `test_site_system_sections`, `test_site_systems`, `test_site_validation`, `test_site_weight`, `test_site_wiki`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_version_basis`, `test_workflow_paths` |
|
||||
| Games on the card | small DATs, listxml and setup sources, a fake fetcher, fixture install lists, and the committed data against its schemas | `test_arcade_parents`, `test_game_catalogs`, `test_game_headers`, `test_game_regions`, `test_games_index`, `test_romfolders_card`, `test_romfolders_es` |
|
||||
|
||||
## Test architecture
|
||||
|
||||
@@ -27,8 +27,7 @@ If step 3 fails, the screen says "BIOS missing" or stays black.
|
||||
## Which consoles need one? { #do-i-need-one }
|
||||
|
||||
- **Yes:** PlayStation 2, Neo Geo, PC Engine CD and many home computers.
|
||||
- **Most emulators:** PlayStation 1 and Saturn. Some start games without it,
|
||||
with a built-in replacement. The emulator's page says which.
|
||||
- **Most emulators:** PlayStation 1 and Saturn.
|
||||
- **Better with it:** Dreamcast. Some games need it.
|
||||
- **Usually not:** NES, Super Nintendo, Mega Drive, Game Boy and Game Boy
|
||||
Advance.
|
||||
@@ -50,13 +49,10 @@ another for Europe.
|
||||
Some setups check each file's **hash**, a fingerprint of its contents.
|
||||
Batocera, Recalbox and EmuDeck report a version they do not expect.
|
||||
|
||||
Where the collection holds a file, it is the version each setup expects.
|
||||
[What is still missing](../gaps.md) names the rest.
|
||||
|
||||
## Can a game start without it?
|
||||
|
||||
Sometimes. Some emulators carry their own code in place of the BIOS. Some games
|
||||
then glitch or do not start. The real file works best, so the packs include it.
|
||||
then glitch or do not start.
|
||||
|
||||
## Is it legal?
|
||||
|
||||
|
||||
Reference in new issue
Block a user