chore: merge the short one-liner

This commit is contained in:
Abdessamad Derraz committed 2026-10-28 05:48:11 +01:00
commit 825863b8f9
37 files changed
+486 -110

No files matched your search

+5
View File
@@ -45,6 +45,7 @@ on:
- "scripts/restore_large_files.py"
- "scripts/safeparse.py"
- "scripts/sect233r1.py"
- "scripts/sitebootstrap.py"
- "scripts/sitediagrams.py"
- "scripts/siteentries.py"
- "scripts/siteguides.py"
@@ -69,7 +70,11 @@ on:
- "schemas/**"
- "docs_assets/**"
- "install/**"
# The site serves install.sh and install.ps1 pinned to install.py
# of the commit it is built from (scripts/sitebootstrap.py).
- "install.py"
- "install.sh"
- "install.ps1"
- "database.json"
- "release.json"
- "derivable.json"
+5 -5
View File
@@ -19,21 +19,21 @@ Copy one command into your terminal:
```bash
# Linux / macOS / Steam Deck
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex
irm https://abdess.github.io/retrobios/i.ps1 | iex
# Android (Termux, after pkg install python and termux-setup-storage)
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
# SD card mounted on a PC: a muOS, KNULLI, ROCKNIX, Onion, spruceOS or MinUI card
# is found by itself. For another card, --dest is its BIOS folder
# (RetroArch on Switch: retroarch/cores/system, on Android: RetroArch/system)
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
# Options in PowerShell: anything after | iex goes to iex, so the script runs as a block
&([scriptblock]::Create((irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1))) --check
&([scriptblock]::Create((irm https://abdess.github.io/retrobios/i.ps1))) --check
```
The script opens a full-screen installer, driven with the arrow keys and Enter, or with a controller on a handheld. It finds your setup and its BIOS folder, downloads only the missing files and checks each one. It can also copy the BIOS files from your old SD card.
+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "7029f605591f09c020aafcd521d8a7134ace78cc566435d8e76be33c42a9f6dc"
$defaultInstallSha256 = "6467b259f1fff162581df002d95f17a2af8b2484e4524dd094003e6ee62217c7"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+4 -2
View File
@@ -243,7 +243,9 @@ CARD_PLATFORMS = frozenset(name for name, _file, _prefix in _CARD_RULES)
SITE_URL = "https://abdess.github.io/retrobios/"
INSTALLER_PAGE = f"{SITE_URL}wiki/installer/"
TROUBLESHOOTING_PAGE = f"{SITE_URL}wiki/troubleshooting/"
BOOTSTRAP_URL = "https://raw.githubusercontent.com/Abdess/retrobios/main/install"
# The one-line bootstraps the site serves: BOOTSTRAP_URL for sh, .ps1 after it
# for PowerShell (scripts/sitebootstrap.py).
BOOTSTRAP_URL = f"{SITE_URL}i"
# Fallback BIOS destination per platform when --platform is forced
# but auto-detection finds nothing on the machine.
@@ -5747,7 +5749,7 @@ class Invocation:
if bootstrap == "ps1":
return f"&([scriptblock]::Create((irm {BOOTSTRAP_URL}.ps1)))"
if bootstrap == "sh":
return f"curl -fsSL {BOOTSTRAP_URL}.sh | {'sudo ' if elevated else ''}sh -s --"
return f"curl -fsSL {BOOTSTRAP_URL} | {'sudo ' if elevated else ''}sh -s --"
python = "python" if self.os_type == "windows" else "python3"
return f"{'sudo ' if elevated else ''}{python} install.py"
+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="7029f605591f09c020aafcd521d8a7134ace78cc566435d8e76be33c42a9f6dc"
DEFAULT_INSTALL_SHA256="6467b259f1fff162581df002d95f17a2af8b2484e4524dd094003e6ee62217c7"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 881442,
"sha256": "7029f605591f09c020aafcd521d8a7134ace78cc566435d8e76be33c42a9f6dc",
"size": 881512,
"sha256": "6467b259f1fff162581df002d95f17a2af8b2484e4524dd094003e6ee62217c7",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 934234,
"sha256": "56675adcad36deb03879b7f5961203f6c65cc2f4f13170f6d38b37ed1fe5b367"
"size": 934304,
"sha256": "2d94d307ff407a842f4a7c8bbd98bc4cfcee27713e6ee88c94339b689dd7dbe0"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 881442,
"sha256": "7029f605591f09c020aafcd521d8a7134ace78cc566435d8e76be33c42a9f6dc",
"size": 881512,
"sha256": "6467b259f1fff162581df002d95f17a2af8b2484e4524dd094003e6ee62217c7",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 881442,
"sha256": "7029f605591f09c020aafcd521d8a7134ace78cc566435d8e76be33c42a9f6dc",
"size": 881512,
"sha256": "6467b259f1fff162581df002d95f17a2af8b2484e4524dd094003e6ee62217c7",
"executable": false
},
{
+2 -1
View File
@@ -19,7 +19,8 @@
# The registry keeps what tools read (device_group, extract_to, install);
# this file keeps what people read.
install_base: https://raw.githubusercontent.com/Abdess/retrobios/main
# The site serving the one-line bootstraps (scripts/sitebootstrap.py).
install_base: https://abdess.github.io/retrobios
# Is it safe: shown on the home page and under every command on a guide page.
# Where it writes follows install.py: the BIOS folder, the frontend's save
+1 -2
View File
@@ -89,8 +89,7 @@ class Pack:
# are served file by file by the installer.
NO_PACK_HINT = (
"A setup without a pack is served by the installer:\n"
" curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh"
" | sh -s -- --platform <name>"
" curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform <name>"
)
+1 -1
View File
@@ -116,7 +116,7 @@ EOF
echo "Available platforms:"
platform_names "$release_json"
echo "A setup without a pack is served by the installer:"
echo " curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform <name>"
echo " curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform <name>"
exit 1
fi
+6 -5
View File
@@ -35,6 +35,7 @@ from common import (
write_if_changed,
)
import release_record
from siteguides import install_command
from verify import verify_platform
@@ -557,21 +558,21 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
"",
"```bash",
"# Linux / macOS / Steam Deck",
"curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh",
install_command("sh"),
"",
"# Windows (PowerShell)",
"irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex",
install_command("powershell"),
"",
"# Android (Termux, after pkg install python and termux-setup-storage)",
"curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh",
install_command("sh"),
"",
"# SD card mounted on a PC: a muOS, KNULLI, ROCKNIX, Onion, spruceOS or MinUI card",
"# is found by itself. For another card, --dest is its BIOS folder",
"# (RetroArch on Switch: retroarch/cores/system, on Android: RetroArch/system)",
"curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system",
install_command("sh", "--platform retroarch --dest /path/to/sdcard/retroarch/cores/system"),
"",
"# Options in PowerShell: anything after | iex goes to iex, so the script runs as a block",
"&([scriptblock]::Create((irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1))) --check",
install_command("powershell", "--check"),
"```",
"",
"The script opens a full-screen installer, driven with the arrow keys and Enter, or with a"
+23 -2
View File
@@ -99,6 +99,8 @@ from siteguides import (
HandheldSystem,
FENCE_TITLES,
INSTALL_BASE,
INSTALL_PS1,
INSTALL_SH,
TERMINALS,
TRUST_ITEMS,
detected_systems as _detected_systems,
@@ -111,6 +113,7 @@ from siteguides import (
trust_box,
)
import release_record
import sitebootstrap
import sitemeasurements
# The Starlight project, and where its pages go inside it.
@@ -6101,6 +6104,23 @@ SYSTEM_PAGE_BUDGET = 300_000
CARD_HTML_RATIO = 1.7
INSTALL_HTML_RATIO = 11
SYSTEM_SUMMARY_HTML = 500_000
# The ratio was fitted while each command named the raw.githubusercontent.com
# bootstrap, 66 characters (67 for PowerShell). A box is weighed with its
# address at that length, so the shorter site address moves no console off
# or onto a summary and no card changes address.
FITTED_ADDRESS_CHARS = {INSTALL_PS1: 67, INSTALL_SH: 66}
def _install_box_weight(lines: list[str]) -> int:
weight = 0
for line in lines:
weight += len(line) + 1
for address, fitted in FITTED_ADDRESS_CHARS.items():
found = line.count(address)
line = line.replace(address, "")
weight += found * (fitted - len(address))
return weight
# A system card: whether it is a variant, its lines, its sha1.
Card = tuple[bool, list[str], str]
@@ -7252,8 +7272,7 @@ def system_pages(
# page, which the card weights never counted. A page is also cut when
# its estimate as it renders passes the summary's.
with markup.using("mkdocs"):
boxes = {console: sum(len(line) + 1 for line in install_block(console))
for console in consoles}
boxes = {console: _install_box_weight(install_block(console)) for console in consoles}
rendered = {console: CARD_HTML_RATIO * weights[console] + INSTALL_HTML_RATIO * boxes[console]
for console in weights}
if (not boards and len(measured_text) <= budget
@@ -11763,6 +11782,8 @@ def _generate(args: argparse.Namespace) -> None:
# this project site lives under /retrobios/. Every page's head points at
# the sitemap instead (Starlight's head links it).
(public / "robots.txt").unlink(missing_ok=True)
# The one-line commands' bootstraps, at the site's root.
sitebootstrap.write_bootstraps(public, _REPO_ROOT)
images_dest = public / "assets" / "images"
images_dest.mkdir(parents=True, exist_ok=True)
+1 -4
View File
@@ -5,10 +5,7 @@ goes, and which ones the user has to rename."""
from __future__ import annotations
from siteguides import INSTALL_BASE, pack_manual
INSTALL_SH = f"{INSTALL_BASE}/install.sh"
INSTALL_PS1 = f"{INSTALL_BASE}/install.ps1"
from siteguides import INSTALL_PS1, INSTALL_SH, pack_manual
def _detect_systems(detect: list[dict]) -> list[str]:
"""The operating systems the registry's detection rules cover, in order."""
+3 -3
View File
@@ -28,9 +28,9 @@ from common import (
load_platform_registry,
write_text_atomic,
)
from siteguides import install_command
REPO = "Abdess/retrobios"
RAW = f"https://raw.githubusercontent.com/{REPO}/main"
SITE = "https://abdess.github.io/retrobios/"
@@ -112,12 +112,12 @@ def render(record: dict, platforms_dir: str, whats_new: str) -> str:
"",
"**Linux, macOS, Steam Deck**",
"```sh",
f"curl -fsSL {RAW}/install.sh | sh",
install_command("sh"),
"```",
"",
"**Windows (PowerShell)**",
"```powershell",
f"irm {RAW}/install.ps1 | iex",
install_command("powershell"),
"```",
"",
"### Download a pack",
+75
View File
@@ -0,0 +1,75 @@
"""The one-line bootstraps the site serves at its own address.
`curl -fsSL https://abdess.github.io/retrobios/i | sh` and
`irm https://abdess.github.io/retrobios/i.ps1 | iex` fetch copies of
install.sh and install.ps1 written when the site is built. Each copy
downloads install.py from the commit the site is built from, the commit
whose bootstrap pins that installer's SHA-256. The site deploys minutes
after a push: until then it serves the previous commit's pair, never the
bootstrap of one commit with the installer of the next.
GitHub Pages serves both names as application/octet-stream (no type in
mime-db for .ps1 or for a bare name). curl ignores the type, and
Invoke-RestMethod reads any type that is neither JSON nor XML as text.
"""
from __future__ import annotations
import os
import re
import subprocess
from pathlib import Path
from artifacts import write_if_changed
REPO_RAW = "https://raw.githubusercontent.com/Abdess/retrobios"
SH_NAME = "i"
PS1_NAME = "i.ps1"
# Served name: the bootstrap it copies and the one line naming the
# installer's address there.
SOURCES: dict[str, tuple[str, re.Pattern[str]]] = {
SH_NAME: ("install.sh", re.compile(r'^(DEFAULT_INSTALL_URL=")[^"]*(")', re.M)),
PS1_NAME: ("install.ps1", re.compile(r'^(\s*\$defaultInstallUrl = ")[^"]*(")', re.M)),
}
def build_commit(root: Path) -> str:
"""The commit the site is built from: GITHUB_SHA in CI, HEAD locally."""
commit = os.environ.get("GITHUB_SHA", "").strip()
if not commit:
commit = subprocess.run(
["git", "rev-parse", "HEAD"], cwd=root, check=True,
capture_output=True, text=True,
).stdout.strip()
if not re.fullmatch(r"[0-9a-f]{40}", commit):
raise ValueError(f"not a commit id: {commit!r}")
return commit
def installer_url(commit: str) -> str:
return f"{REPO_RAW}/{commit}/install.py"
def served_text(name: str, root: Path, commit: str) -> str:
"""The bootstrap served as name, its installer read from commit."""
source, line = SOURCES[name]
text = (root / source).read_text(encoding="utf-8")
served, count = line.subn(
lambda m: f"{m.group(1)}{installer_url(commit)}{m.group(2)}", text
)
if count != 1:
raise ValueError(f"{source}: {count} installer address lines, expected one")
return served
def write_bootstraps(public: Path, root: Path, commit: str | None = None) -> list[Path]:
"""Write every served bootstrap at the root of the site's public folder."""
commit = commit or build_commit(root)
public.mkdir(parents=True, exist_ok=True)
written = []
for name in SOURCES:
path = public / name
write_if_changed(str(path), served_text(name, root, commit))
written.append(path)
return written
+2 -2
View File
@@ -750,10 +750,10 @@ FENCE_TITLES = {"sh": "Terminal", "powershell": "PowerShell"}
def _installer_args(body: str) -> str:
"""The options of a one-line install command, continuation lines joined."""
from siteguides import INSTALL_BASE
from siteguides import INSTALL_SH
line = " ".join(part.strip().rstrip("\\").strip() for part in body.splitlines())
match = re.fullmatch(rf"curl -fsSL {re.escape(INSTALL_BASE)}/install\.sh \| sh(?: -s -- (.+))?", line)
match = re.fullmatch(rf"curl -fsSL {re.escape(INSTALL_SH)} \| sh(?: -s -- (.+))?", line)
if not match:
raise ValueError(f"an os-tabs block holds no install command: {line!r}")
return match.group(1) or ""
+7 -3
View File
@@ -18,6 +18,7 @@ from pathlib import Path
import sitemarkup as markup
from safeparse import require_yaml
from sitebootstrap import PS1_NAME, SH_NAME
PLATFORMS_DIR = Path(__file__).resolve().parent.parent / "platforms"
GUIDES_FILE = PLATFORMS_DIR / "_guides.yml"
@@ -380,6 +381,9 @@ def load_guides(path: Path = GUIDES_FILE, registry: dict | None = None) -> dict:
_LOADED = load_guides()
INSTALL_BASE: str = _LOADED["install_base"]
# The bootstraps the site serves (sitebootstrap.py writes them).
INSTALL_SH: str = f"{INSTALL_BASE}/{SH_NAME}"
INSTALL_PS1: str = f"{INSTALL_BASE}/{PS1_NAME}"
DEVICE_GROUPS: dict[str, tuple[str, str]] = _LOADED["device_groups"]
GROUP_ANCHORS: dict[str, str] = _LOADED["group_anchors"]
ALSO_RUNS: dict[str, tuple[tuple[str, str, str, str, str], ...]] = _LOADED["also_runs"]
@@ -435,10 +439,10 @@ def install_command(shell: str, args: str = "") -> str:
takes none.
"""
if shell == "sh":
return f"curl -fsSL {INSTALL_BASE}/install.sh | sh" + (f" -s -- {args}" if args else "")
return f"curl -fsSL {INSTALL_SH} | sh" + (f" -s -- {args}" if args else "")
if args:
return f"&([scriptblock]::Create((irm {INSTALL_BASE}/install.ps1))) {args}"
return f"irm {INSTALL_BASE}/install.ps1 | iex"
return f"&([scriptblock]::Create((irm {INSTALL_PS1}))) {args}"
return f"irm {INSTALL_PS1} | iex"
# The title over each command block, by the shell it is written for.
+2 -1
View File
@@ -404,6 +404,7 @@ def _check_get_pages(
"""
sys.path.insert(0, str(Path(__file__).resolve().parent))
import release_record # noqa: PLC0415
from siteguides import INSTALL_SH # noqa: PLC0415
from common import list_registered_platforms, list_release_pack_platforms # noqa: PLC0415
if not (platforms_dir / "_registry.yml").is_file():
@@ -428,7 +429,7 @@ def _check_get_pages(
packs = [a for a in assets if "_BIOS_Pack" in a]
if packs:
issues.append(f"{rel}: links a pack for a setup without one: {packs[0]}")
if "install.sh" not in (site / rel).read_text(encoding="utf-8"):
if f"{INSTALL_SH} | sh" not in (site / rel).read_text(encoding="utf-8"):
issues.append(f"{rel}: gives no install command")
return issues
+19 -7
View File
@@ -59,14 +59,22 @@ class ReadmeRegressions(unittest.TestCase):
)[0]
def test_advertised_bootstrap_urls_are_the_shipped_ones(self):
import sitebootstrap
from siteguides import INSTALL_BASE
quick_install = self._quick_install()
for script in ("install.sh", "install.ps1"):
url = f"https://raw.githubusercontent.com/Abdess/retrobios/main/{script}"
self.assertIn(url, quick_install, f"{script} bootstrap URL missing")
for name in sitebootstrap.SOURCES:
url = f"{INSTALL_BASE}/{name}"
self.assertIn(f"{url} |", quick_install, f"{name} bootstrap URL missing")
# The site serves each bootstrap with install.py read from the
# commit it was built from, the raw form from main.
commit = "0" * 40
for name in sitebootstrap.SOURCES:
self.assertIn(sitebootstrap.installer_url(commit),
sitebootstrap.served_text(name, ROOT, commit))
self.assertIn(
"https://raw.githubusercontent.com/Abdess/retrobios/main/install.py",
(ROOT / "install.sh").read_text(encoding="utf-8"),
"install.sh must fetch install.py from the ref the README advertises",
)
def test_advertised_installer_flags_exist(self):
@@ -690,9 +698,11 @@ class PackReadmesSayWhatTheInstallerDoes(unittest.TestCase):
return text[text.index("INSTALLATION"):text.index("TROUBLESHOOTING")]
def test_retroarch_offers_the_windows_installer(self):
from siteguides import INSTALL_PS1, INSTALL_SH
guide = self._guide("retroarch")
self.assertIn("install.sh | sh", guide)
self.assertIn("install.ps1 | iex", guide)
self.assertIn(f"{INSTALL_SH} | sh", guide)
self.assertIn(f"{INSTALL_PS1} | iex", guide)
def test_emudeck_keys_match_the_registry_and_the_flag(self):
import re
@@ -715,8 +725,10 @@ class PackReadmesSayWhatTheInstallerDoes(unittest.TestCase):
def test_retropie_never_sends_a_pc_to_the_linux_partition(self):
guide = self._guide("retropie")
from siteguides import INSTALL_SH
self.assertNotIn("Put the SD card in your PC", guide)
self.assertIn("install.sh | sh", guide)
self.assertIn(f"{INSTALL_SH} | sh", guide)
class AutoFetchReadsListsAndPlacesWithoutClobbering(unittest.TestCase):
+4 -2
View File
@@ -663,12 +663,14 @@ class ReadmeInstallerOptions(unittest.TestCase):
import generate_readme
source = inspect.getsource(generate_readme.generate_readme)
self.assertIn("&([scriptblock]::Create((irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1)))", source)
self.assertIn('install_command("powershell", "--check")', source)
self.assertIn("wiki/installer/#passing-options", source)
self.assertIn("## Passing options", _page("installer.md"))
readme = (ROOT / "README.md").read_text(encoding="utf-8")
quick = _section(readme, "Quick install")
self.assertIn("[scriptblock]::Create", quick)
from siteguides import INSTALL_PS1
self.assertIn(f"&([scriptblock]::Create((irm {INSTALL_PS1}))) --check", quick)
self.assertIn("wiki/installer/#passing-options", quick)
self.assertNotRegex(readme, r"(?m)^irm \S+ \| iex \S")
+1 -1
View File
@@ -267,7 +267,7 @@ class EndUsersCheckTheirOwnFolder(unittest.TestCase):
def test_the_download_page_gives_the_one_liner_check(self):
page = _which_pack()
self.assertNotIn("from a checkout", page)
self.assertIn("install.sh | sh -s -- --check", page)
self.assertIn("/i | sh -s -- --check", page)
def test_the_readme_does_not_call_verify_a_check_of_your_bios(self):
self.assertNotIn("Verify your BIOS collection", _read("README.md"))
+4 -4
View File
@@ -1911,7 +1911,7 @@ class TestManualPlatformPrompt(unittest.TestCase):
with unittest.mock.patch.dict(os.environ, {"RETROBIOS_BOOTSTRAP": "ps1"}):
hint = "\n".join(install._manual_usage_hint("windows"))
self.assertIn("--platform retroarch --dest", hint)
self.assertIn("install.ps1", hint)
self.assertIn("/i.ps1", hint)
class TestSteamRetroarchDetection(unittest.TestCase):
@@ -3983,7 +3983,7 @@ class HintsGiveTheUsersOwnCommandBack(unittest.TestCase):
def test_the_shell_one_liner_is_rebuilt(self):
with unittest.mock.patch.dict(os.environ, {"RETROBIOS_BOOTSTRAP": "sh"}):
hint = "\n".join(install._manual_usage_hint("linux"))
self.assertIn("install.sh | sh -s -- --platform retroarch", hint)
self.assertIn("/i | sh -s -- --platform retroarch", hint)
self.assertNotIn("python3 install.py", hint)
self.assertIn(install.INSTALLER_PAGE, hint)
@@ -4028,7 +4028,7 @@ class HintsGiveTheUsersOwnCommandBack(unittest.TestCase):
False, "retroarch")
self.assertNotIn("sudo", advice)
self.assertIn("administrator", advice)
self.assertIn("install.ps1", advice)
self.assertIn("/i.ps1", advice)
self.assertIn("--system atari-lynx", advice)
def test_a_full_drive_keeps_the_platform(self):
@@ -4070,7 +4070,7 @@ class HintsGiveTheUsersOwnCommandBack(unittest.TestCase):
proc = _install_run(env, "--help")
self.assertEqual(proc.returncode, 0)
self.assertIn("examples:", proc.stdout)
self.assertIn("install.sh | sh -s -- --platform batocera --dest", proc.stdout)
self.assertIn("/i | sh -s -- --platform batocera --dest", proc.stdout)
self.assertIn(install.INSTALLER_PAGE, proc.stdout)
+3 -5
View File
@@ -9930,14 +9930,12 @@ class TheAndroidResultReadsWhole(unittest.TestCase):
self.assertIn("--restore", words)
self.assertTrue(all(not word.endswith("/") for word in words), words)
state = _installed_state(Path("/sdcard/RetroArch/system"))
command = install._styled("curl -fsSL https://raw.githubusercontent.com/Abdess/"
"retrobios/main/install.sh | sh -s -- --restore "
command = install._styled(f"curl -fsSL {install.BOOTSTRAP_URL} | sh -s -- --restore "
"'/storage/emulated/0/a b/20261027-223524'", "command")
state.stack.append("result")
pieces = install._text_lines([command], 38, whole=True)
joined = [str(piece).strip() for piece in pieces if piece.style != "cont"]
self.assertIn("https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh",
joined)
self.assertIn(install.BOOTSTRAP_URL, joined)
self.assertIn("'/storage/emulated/0/a b/20261027-223524'", joined)
def test_a_long_line_folds_in_the_terminal(self):
@@ -10770,7 +10768,7 @@ class AuditNineteenAndTwelve(_CardFixtures, unittest.TestCase):
lines = drawn(state, cols, 30)
self.assertFalse(install.tui_wants_mouse(state, lines))
if bootstrap:
url = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh"
url = install.BOOTSTRAP_URL
words = [word for line in lines for word in str(line).split()]
self.assertIn(url, words)
+7 -5
View File
@@ -75,8 +75,8 @@ class TheOneLinerFollowsTheRegistry(unittest.TestCase):
continue
text = _guide(name)
with self.subTest(platform=name):
self.assertEqual("install.sh | sh" in text, bool(systems - {"windows"}))
self.assertEqual("install.ps1 | iex" in text, "windows" in systems)
self.assertEqual("/i | sh" in text, bool(systems - {"windows"}))
self.assertEqual("/i.ps1 | iex" in text, "windows" in systems)
def test_rocknix_and_mister_run_it_on_the_device(self):
for name in ("rocknix", "misterfpga"):
@@ -87,7 +87,7 @@ class TheOneLinerFollowsTheRegistry(unittest.TestCase):
text = _guide("emudeck")
self.assertNotIn("Steam Deck / Linux", text)
self.assertNotIn("Konsole", text)
self.assertIn("install.ps1 | iex", text)
self.assertIn("/i.ps1 | iex", text)
self.assertIn("C:\\Emulation\\bios", text)
self.assertIn("--standalone-copies", text)
@@ -101,12 +101,14 @@ class TheOneLinerFollowsTheRegistry(unittest.TestCase):
"""The address comes from install_base in platforms/_guides.yml, the
one the site's commands use."""
import packreadme
import sitebootstrap
from siteguides import INSTALL_BASE
source = (REPO_ROOT / "scripts" / "packreadme.py").read_text(encoding="utf-8")
self.assertNotIn("raw.githubusercontent.com", source)
self.assertEqual(packreadme.INSTALL_SH, f"{INSTALL_BASE}/install.sh")
self.assertEqual(packreadme.INSTALL_PS1, f"{INSTALL_BASE}/install.ps1")
self.assertNotIn("abdess.github.io", source)
self.assertEqual(packreadme.INSTALL_SH, f"{INSTALL_BASE}/{sitebootstrap.SH_NAME}")
self.assertEqual(packreadme.INSTALL_PS1, f"{INSTALL_BASE}/{sitebootstrap.PS1_NAME}")
if __name__ == "__main__":
+1 -1
View File
@@ -321,7 +321,7 @@ class EveryPlatformHasAPermanentPage(unittest.TestCase):
f"/releases/download/{record['tag']}/{asset}", page)
elif name not in RELEASED:
self.assertNotIn("_BIOS_Pack", page)
self.assertIn(f"install.sh | sh -s -- --platform {name}", page)
self.assertIn(f"/i | sh -s -- --platform {name}", page)
self.assertIn("No ZIP for ", page)
def test_the_generator_writes_and_sweeps_them(self):
+232
View File
@@ -0,0 +1,232 @@
"""The one-line commands the site serves.
`curl -fsSL https://abdess.github.io/retrobios/i | sh` and the PowerShell
`i.ps1` replaced a raw.githubusercontent.com address of 70 characters. The
site deploys some eleven minutes after a push: a copy that pinned one
commit's installer and downloaded another's would refuse every install for
that long, so each copy reads install.py from the commit it was built from.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import install # noqa: E402
import sitebootstrap # noqa: E402
from siteguides import INSTALL_BASE, INSTALL_PS1, INSTALL_SH, install_command # noqa: E402
WEBSITE = REPO_ROOT / "website"
DOCS = WEBSITE / "src" / "content" / "docs"
OLD_SH = "raw.githubusercontent.com/Abdess/retrobios/main/install.sh"
OLD_PS1 = "raw.githubusercontent.com/Abdess/retrobios/main/install.ps1"
def _head() -> str:
return subprocess.run(["git", "rev-parse", "HEAD"], cwd=REPO_ROOT, check=True,
capture_output=True, text=True).stdout.strip()
def _pin(text: str) -> str:
match = re.search(r'(?:DEFAULT_INSTALL_SHA256="|\$defaultInstallSha256 = ")([0-9a-f]{64})"',
text)
assert match, "no installer pin"
return match.group(1)
class ServedCopies(unittest.TestCase):
def setUp(self):
self.commit = _head()
def test_each_copy_is_its_bootstrap_but_for_the_installer_address(self):
for name, (source, _line) in sitebootstrap.SOURCES.items():
with self.subTest(name=name):
original = (REPO_ROOT / source).read_text(encoding="utf-8").splitlines()
served = sitebootstrap.served_text(name, REPO_ROOT, self.commit).splitlines()
self.assertEqual(len(original), len(served))
changed = [(a, b) for a, b in zip(original, served) if a != b]
self.assertEqual(len(changed), 1, changed)
before, after = changed[0]
self.assertIn("https://raw.githubusercontent.com/Abdess/retrobios/main/install.py",
before)
self.assertEqual(after, before.replace(
"https://raw.githubusercontent.com/Abdess/retrobios/main/install.py",
sitebootstrap.installer_url(self.commit)))
def test_the_address_names_the_commit_whose_installer_the_pin_holds(self):
installer = hashlib.sha256((REPO_ROOT / "install.py").read_bytes()).hexdigest()
url = f"https://raw.githubusercontent.com/Abdess/retrobios/{self.commit}/install.py"
for name in sitebootstrap.SOURCES:
with self.subTest(name=name):
text = sitebootstrap.served_text(name, REPO_ROOT, self.commit)
self.assertIn(f'"{url}"', text)
self.assertEqual(_pin(text), installer)
def test_ci_builds_from_the_pushed_commit(self):
pushed = "a" * 40
with mock.patch.dict(os.environ, {"GITHUB_SHA": pushed}):
self.assertEqual(sitebootstrap.build_commit(REPO_ROOT), pushed)
with mock.patch.dict(os.environ, {"GITHUB_SHA": "main"}), \
self.assertRaises(ValueError):
sitebootstrap.build_commit(REPO_ROOT)
with mock.patch.dict(os.environ, {"GITHUB_SHA": ""}):
self.assertEqual(sitebootstrap.build_commit(REPO_ROOT), self.commit)
def test_the_copies_land_at_the_root_of_the_public_folder(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp" if (REPO_ROOT / "tmp").is_dir()
else None) as scratch:
public = Path(scratch) / "public"
written = sitebootstrap.write_bootstraps(public, REPO_ROOT, self.commit)
self.assertEqual(sorted(p.name for p in written), ["i", "i.ps1"])
for path in written:
self.assertEqual(path.read_text(encoding="utf-8"),
sitebootstrap.served_text(path.name, REPO_ROOT, self.commit))
def test_a_bootstrap_without_its_address_line_stops_the_build(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp" if (REPO_ROOT / "tmp").is_dir()
else None) as scratch:
root = Path(scratch)
(root / "install.sh").write_text("#!/bin/sh\n", encoding="utf-8")
with self.assertRaises(ValueError):
sitebootstrap.served_text("i", root, self.commit)
class DocumentedCommands(unittest.TestCase):
def test_the_addresses_are_the_served_paths(self):
config = (WEBSITE / "astro.config.mjs").read_text(encoding="utf-8")
site = re.search(r"const SITE = '([^']+)'", config).group(1)
base = re.search(r"const BASE = '([^']+)'", config).group(1)
self.assertEqual(f"{INSTALL_BASE}/", site + base)
self.assertEqual(INSTALL_SH, f"{site}{base}{sitebootstrap.SH_NAME}")
self.assertEqual(INSTALL_PS1, f"{site}{base}{sitebootstrap.PS1_NAME}")
self.assertEqual(install.BOOTSTRAP_URL, INSTALL_SH)
self.assertEqual(f"{install.BOOTSTRAP_URL}.ps1", INSTALL_PS1)
def test_the_installer_hints_give_the_short_command(self):
with mock.patch.dict(os.environ, {"RETROBIOS_BOOTSTRAP": "sh"}):
self.assertEqual(install.Invocation().launcher(), f"curl -fsSL {INSTALL_SH} | sh -s --")
with mock.patch.dict(os.environ, {"RETROBIOS_BOOTSTRAP": "ps1"}):
self.assertEqual(install.Invocation().launcher(),
f"&([scriptblock]::Create((irm {INSTALL_PS1})))")
def test_every_page_a_user_copies_from_gives_the_short_command(self):
sources = {
"wiki/installer.md": [install_command("sh"), install_command("powershell")],
"wiki/first-install.md": [install_command("sh"), install_command("powershell")],
"wiki/faq.md": [f"curl -fsSL {INSTALL_SH} | sh -s -- --dry-run"],
"wiki/troubleshooting.md": [install_command("sh", "--check")],
"wiki/tools.md": [install_command("sh"), install_command("powershell")],
"website/src/components/FilePicker.astro": [f"'curl -fsSL {INSTALL_SH} | sh -s --'"],
"scripts/download.sh": [f"curl -fsSL {INSTALL_SH} | sh -s -- --platform <name>"],
"scripts/download.py": [f"curl -fsSL {INSTALL_SH} | sh -s -- --platform <name>"],
}
for rel, commands in sources.items():
text = (REPO_ROOT / rel).read_text(encoding="utf-8")
for command in commands:
with self.subTest(file=rel, command=command):
self.assertIn(command, text)
def test_the_raw_form_is_written_once_as_the_fallback(self):
"""The long address stays on the installer page only, for the day
the site is down."""
surfaces = [*sorted((REPO_ROOT / "wiki").glob("*.md")), REPO_ROOT / "README.md",
REPO_ROOT / "CONTRIBUTING.md", REPO_ROOT / "install.py",
*sorted((REPO_ROOT / "scripts").rglob("*.py")),
REPO_ROOT / "scripts" / "download.sh",
*sorted((WEBSITE / "src" / "components").glob("*.astro"))]
found = {
str(path.relative_to(REPO_ROOT)): text.count(OLD_SH) + text.count(OLD_PS1)
for path in surfaces
for text in [path.read_text(encoding="utf-8")]
if OLD_SH in text or OLD_PS1 in text
}
self.assertEqual(found, {"wiki/installer.md": 2})
def test_typed_by_hand_without_the_scheme(self):
page = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
bare = INSTALL_BASE.removeprefix("https://")
self.assertIn(f"curl -fL {bare}/{sitebootstrap.SH_NAME} | sh", page)
self.assertIn(f"irm {bare}/{sitebootstrap.PS1_NAME} | iex", page)
class NothingElseAtTheseAddresses(unittest.TestCase):
"""A page or a folder named i would take the address, or turn it into a
redirect to i/ that `curl | sh` reads as HTML."""
NAMES = tuple(sitebootstrap.SOURCES)
def test_no_source_page_takes_the_names(self):
for folder in (REPO_ROOT / "wiki", WEBSITE / "src" / "pages"):
taken = sorted(str(path.relative_to(REPO_ROOT)) for path in folder.glob("i*")
if path.name == "i" or path.name.split(".", 1)[0] == "i")
with self.subTest(folder=str(folder.relative_to(REPO_ROOT))):
self.assertEqual(taken, [])
@unittest.skipUnless(DOCS.is_dir(), "site not generated")
def test_no_generated_page_or_redirect_takes_the_names(self):
for name in self.NAMES:
stem = name.split(".", 1)[0]
with self.subTest(name=name):
self.assertFalse((DOCS / stem).exists())
self.assertEqual(sorted(DOCS.glob(f"{stem}.md*")), [])
redirects = WEBSITE / "src" / "generated" / "redirects.json"
if redirects.is_file():
sources = {key.strip("/").removeprefix("retrobios/")
for key in json.loads(redirects.read_text(encoding="utf-8"))}
self.assertEqual(sources & set(self.NAMES), set())
@unittest.skipUnless((WEBSITE / "dist" / "index.html").is_file(), "site not built")
def test_the_built_site_serves_files_at_the_addresses(self):
for name in self.NAMES:
path = WEBSITE / "dist" / name
with self.subTest(name=name):
self.assertTrue(path.is_file())
text = path.read_text(encoding="utf-8")
self.assertRegex(text, r"raw\.githubusercontent\.com/Abdess/retrobios/[0-9a-f]{40}/"
r"install\.py")
class GeneratedSurfaces(unittest.TestCase):
def test_a_shorter_address_moves_no_section(self):
"""The install boxes weigh in the split of a manufacturer's page,
fitted on the 66-character raw address: weighed at their new length,
Nintendo DS fell back onto the Nintendo summary and lost its page."""
import generate_site
tail = " | sh -s -- --check"
self.assertEqual(generate_site._install_box_weight([f"curl -fsSL {INSTALL_SH}{tail}"]),
len("curl -fsSL ") + 66 + len(tail) + 1)
line = f"&([scriptblock]::Create((irm {INSTALL_PS1}))) --check"
self.assertEqual(generate_site._install_box_weight([line]),
len(line) - len(INSTALL_PS1) + 67 + 1)
def test_the_readme_shows_the_short_form(self):
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
quick = readme.split("## Quick install", 1)[1].split("\n## ", 1)[0]
self.assertIn(f"\n{install_command('sh')}\n", quick)
self.assertIn(f"\n{install_command('powershell')}\n", quick)
self.assertNotIn("raw.githubusercontent.com", quick)
@unittest.skipUnless(DOCS.is_dir(), "site not generated")
def test_the_site_shows_the_short_form(self):
pages = {path: path.read_text(encoding="utf-8") for path in DOCS.rglob("*.md*")}
self.assertTrue(any(INSTALL_SH in text for text in pages.values()))
long_form = sorted(str(path.relative_to(DOCS)) for path, text in pages.items()
if OLD_SH in text or OLD_PS1 in text)
self.assertEqual(long_form, ["wiki/installer.md"])
if __name__ == "__main__":
unittest.main()
+4 -4
View File
@@ -207,10 +207,10 @@ class AnInstallBoxShowsOneCommand(unittest.TestCase):
listed = self.text[self.text.index('<details class="rb-install-all" open>'):]
for platform, _system in PAIRS:
self.assertEqual(listed.count(f"--platform {platform} --system"), 2, platform)
self.assertIn("install.sh | sh -s -- --platform retrobat --system 'Neo Geo'</code>", listed)
self.assertIn("install.ps1))) --platform retrobat --system 'Neo Geo'</code>", listed)
self.assertIn("/i | sh -s -- --platform retrobat --system 'Neo Geo'</code>", listed)
self.assertIn("/i.ps1))) --platform retrobat --system 'Neo Geo'</code>", listed)
# The URL is no link inside the command, and the break adds nothing.
self.assertIn("https://<wbr>raw.githubusercontent.com", listed)
self.assertIn("https://<wbr>abdess.github.io", listed)
def test_one_setup_keeps_its_two_blocks(self) -> None:
text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS[:1]))
@@ -225,7 +225,7 @@ class AnInstallBoxShowsOneCommand(unittest.TestCase):
text = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
self.assertNotIn("<select", text)
self.assertEqual(text.count("# RetroBat"), 2)
self.assertEqual(text.count("install.sh | sh"), 3)
self.assertEqual(text.count("/i | sh"), 3)
class TheSetupListLooksLikeAList(unittest.TestCase):
+6 -6
View File
@@ -902,8 +902,8 @@ class AnEmulatorPageOffersItsInstall(unittest.TestCase):
profile = yaml.safe_load((ROOT / "emulators" / "pcem.yml").read_text(encoding="utf-8"))
text = "\n".join(_install_block("pcem", profile))
self.assertIn("install.sh | sh -s -- --emulator pcem", text)
self.assertIn("install.ps1))) --emulator pcem", text)
self.assertIn("/i | sh -s -- --emulator pcem", text)
self.assertIn("/i.ps1))) --emulator pcem", text)
self.assertEqual(_install_block("x", {"type": "alias"}), [])
def test_the_installer_takes_the_option(self):
@@ -969,7 +969,7 @@ class TheOneLinerIsWhereUsersLand(unittest.TestCase):
page = generate_site.generate_which_pack()
self.assertIn("Android (Termux", page)
self.assertIn("install.ps1))) --check", page)
self.assertIn("/i.ps1))) --check", page)
self.assertIn("--platform retroarch --dest", page)
def test_platform_and_system_pages(self):
@@ -979,14 +979,14 @@ class TheOneLinerIsWhereUsersLand(unittest.TestCase):
cov = {"platform": "Batocera", "mode": "md5", "present": 0, "total": 0,
"verified": 0, "missing": 0, "details": [], "config": {"systems": {}}}
page = generate_site.generate_platform_page("batocera", cov)
self.assertIn("install.sh | sh -s -- --platform batocera", page)
self.assertIn("/i | sh -s -- --platform batocera", page)
# Batocera runs the command on the device itself, so its page opens on
# the computer route. A platform detected on Windows gets the
# PowerShell form, and its command comes before the ZIP.
cov = dict(cov, platform="RetroArch", mode="existence")
page = generate_site.generate_platform_page("retroarch", cov)
self.assertIn("install.ps1))) --platform retroarch", page)
self.assertIn("install.sh | sh -s -- --platform retroarch", page)
self.assertIn("/i.ps1))) --platform retroarch", page)
self.assertIn("/i | sh -s -- --platform retroarch", page)
self.assertLess(page.index("--platform retroarch"), page.index("rb-button"))
entry = {"name": "a.bin", "sha1": "a" * 40, "md5": "b" * 32, "size": 3,
+5 -5
View File
@@ -279,23 +279,23 @@ class CommandBlocks(unittest.TestCase):
def test_shell_lines_have_a_powershell_form(self):
text = _page("which-pack.md")
groups = re.findall(r"<Tabs.*?</Tabs>|<details.*?</details>", text, re.S)
commands = [group for group in groups if "install.sh | sh" in group]
commands = [group for group in groups if "/i | sh" in group]
self.assertGreaterEqual(len(commands), 3)
for group in commands:
self.assertIn("install.ps1", group)
self.assertIn("/i.ps1", group)
block = "\n".join(generate_site._console_install_block(
"PlayStation", [("batocera", "psx"), ("recalbox", "psx")]))
# Every setup's options in both forms: the list of every command,
# and the one command under the OS tabs.
sh_args = re.findall(r"install\.sh \| sh -s -- ([^<\n]+)", block)
ps_args = re.findall(r"install\.ps1\)\)\) ([^<\n]+)", block)
sh_args = re.findall(r"/i \| sh -s -- ([^<\n]+)", block)
ps_args = re.findall(r"/i\.ps1\)\)\) ([^<\n]+)", block)
self.assertEqual(sorted(set(sh_args)), sorted(set(ps_args)))
self.assertEqual(len(set(sh_args)), 2)
self.assertIn('```powershell title="PowerShell"', block)
for key, guide in siteguides.GUIDES.items():
if "windows" in guide.run:
with self.subTest(platform=key):
self.assertIn("install.ps1", _guide_part(_page(f"platforms/{key}.md")))
self.assertIn("/i.ps1", _guide_part(_page(f"platforms/{key}.md")))
class ContributingOpensOnTheAction(unittest.TestCase):
+2 -2
View File
@@ -22,7 +22,7 @@ sys.path.insert(0, str(REPO_ROOT / "scripts"))
sys.path.insert(0, str(REPO_ROOT))
import sitediagrams # noqa: E402
from siteguides import INSTALL_BASE # noqa: E402
from siteguides import INSTALL_SH # noqa: E402
from tests import sitepages # noqa: E402
@@ -108,7 +108,7 @@ class InstallCommandsHaveEveryForm(unittest.TestCase):
def test_a_windows_path_replaces_the_unix_one(self):
page = (
"<!-- os-tabs windows: --platform retroarch --dest E:\\x -->\n```sh\n"
f"curl -fsSL {INSTALL_BASE}/install.sh \\\n | sh -s -- --platform retroarch --dest /a/b\n```\n"
f"curl -fsSL {INSTALL_SH} \\\n | sh -s -- --platform retroarch --dest /a/b\n```\n"
)
text = sitediagrams.expand_commands(page)
windows, rest = text.split('<TabItem label="macOS">', 1)
+2 -2
View File
@@ -22,7 +22,7 @@ sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import sitediagrams # noqa: E402
from siteguides import INSTALL_BASE # noqa: E402
from siteguides import INSTALL_SH # noqa: E402
from tests import sitepages # noqa: E402
WIKI = REPO_ROOT / "wiki"
@@ -113,7 +113,7 @@ class OsTabsTakeOptionsPerSystem(unittest.TestCase):
"""The marker gives any tab its own options, and refuses words it cannot read."""
PAGE = ("<!-- os-tabs windows: --dest E:\\x macos: --dest /m steamdeck: --dest /d -->\n"
f"```sh\ncurl -fsSL {INSTALL_BASE}/install.sh | sh -s -- --dest /l\n```\n")
f"```sh\ncurl -fsSL {INSTALL_SH} | sh -s -- --dest /l\n```\n")
def test_each_named_tab_uses_its_own(self):
parts = re.split(r'<TabItem label="([^"]+)">', sitediagrams.expand_commands(self.PAGE))
+1 -1
View File
@@ -68,7 +68,7 @@ const packs = Object.fromEntries(
type Release = { tag: string; packs: Record<string, { files: number; size: number }> };
const INSTALL = 'curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s --';
const INSTALL = 'curl -fsSL https://abdess.github.io/retrobios/i | sh -s --';
const MAX_ROWS = 500;
// A word that holds a hyphen (--platform, nintendo-64dd) never breaks
// inside: the same rule as the command blocks (ec-unbroken-tokens.mjs).
+2 -2
View File
@@ -20,7 +20,7 @@ terms: ios, iphone, ipad
<!-- os-tabs -->
```sh
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --dry-run
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --dry-run
```
## Do I need every file of a console?
@@ -83,7 +83,7 @@ with the card in drive `E:`, run:
<!-- os-tabs windows: --platform retroarch --dest E:\retroarch\cores\system -->
```sh
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh \
curl -fsSL https://abdess.github.io/retrobios/i \
| sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
```
+7 -7
View File
@@ -20,7 +20,7 @@ line, then press Enter.
Open the Start menu. Type **PowerShell** and open it.
```powershell title="PowerShell"
irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex
irm https://abdess.github.io/retrobios/i.ps1 | iex
```
</TabItem>
@@ -29,7 +29,7 @@ irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex
Open **Terminal**, in Applications, then Utilities.
```sh title="Terminal"
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
```
</TabItem>
@@ -38,7 +38,7 @@ curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh |
Open a terminal.
```sh title="Terminal"
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
```
</TabItem>
@@ -47,7 +47,7 @@ curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh |
Switch to Desktop Mode and open **Konsole**.
```sh title="Terminal"
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
```
</TabItem>
@@ -58,7 +58,7 @@ Install **Termux**, a terminal app, and run these lines.
```sh title="Terminal"
pkg install python
termux-setup-storage
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
```
</TabItem>
@@ -120,7 +120,7 @@ Add `--check` to the same command. It changes nothing.
<!-- os-tabs -->
```sh
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --check
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --check
```
All good:
@@ -154,7 +154,7 @@ $home/RetroPie/BIOS as "bios", and RetroPie docs/Transferring-Roms.md:29 names
<!-- os-tabs windows: --platform retropie --dest \\RETROPIE\bios\ -->
```sh
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retropie --dest <the bios share>
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retropie --dest <the bios share>
```
### Error: the downloaded installer does not match its expected fingerprint.
+34 -7
View File
@@ -29,11 +29,38 @@ Each links to its row in the [Options](#options) table.
```bash
# Linux / macOS / Steam Deck
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
```
```powershell
# Windows
irm https://abdess.github.io/retrobios/i.ps1 | iex
```
Typed by hand, the address works without `https://`:
```bash
curl -fL abdess.github.io/retrobios/i | sh
```
```powershell
irm abdess.github.io/retrobios/i.ps1 | iex
```
The `https://` form is safer, because without it the first request goes out
in clear text and the redirect it gets back could be replaced on the way.
`i` and `i.ps1` are `install.sh` and `install.ps1` as of the commit the site
was built from, set to download `install.py` from that same commit. The
fingerprint they check and the installer they fetch never come from two
different versions. If the site is down, the repository serves the same
bootstraps:
```bash
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
```
```powershell
irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex
```
@@ -68,7 +95,7 @@ What `RETROBIOS_REF` does not pin is listed under
The shell bootstrap forwards everything after `-s --`:
```bash
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh \
curl -fsSL https://abdess.github.io/retrobios/i \
| sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
```
@@ -84,7 +111,7 @@ rejected with `A positional parameter cannot be found`. Building a script block
from the text and calling it passes the arguments through:
```powershell
&([scriptblock]::Create((irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1))) `
&([scriptblock]::Create((irm https://abdess.github.io/retrobios/i.ps1))) `
--platform retroarch --dest D:\bios
```
@@ -1258,7 +1285,7 @@ Running `termux-setup-storage` once is what makes the shared one reachable:
```bash
pkg install python
termux-setup-storage
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
```
`EXTERNAL_STORAGE` overrides the storage root for a device that mounts it
@@ -1735,12 +1762,12 @@ The installer finds a platform from the config files it knows. When it finds
none, name the platform and its BIOS folder:
```bash
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest ~/.config/retroarch/system
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform batocera --dest /userdata/bios
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retroarch --dest ~/.config/retroarch/system
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform batocera --dest /userdata/bios
```
```powershell
&([scriptblock]::Create((irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1))) `
&([scriptblock]::Create((irm https://abdess.github.io/retrobios/i.ps1))) `
--platform retroarch --dest C:\RetroArch-Win64\system
```
+1 -4
View File
@@ -64,10 +64,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, inert run output on the site. 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_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_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_version_basis`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `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_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_entry_tables`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `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_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_version_basis`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `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_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `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_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_availability`, `test_site_bootstrap`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_entry_tables`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_r2_components`, `test_site_r2_css`, `test_site_r2_emulators`, `test_site_r2_entry`, `test_site_r2_gate`, `test_site_r2_nav`, `test_site_r2_p0`, `test_site_r2_reference`, `test_site_r2_systems`, `test_site_r2_wiki`, `test_site_r3_collection`, `test_site_r3_components`, `test_site_r3_css`, `test_site_r3_emulators`, `test_site_r3_entry`, `test_site_r3_nav`, `test_site_r3_p0`, `test_site_r3_reference`, `test_site_r3_systems`, `test_site_r3_wiki`, `test_site_r4_components`, `test_site_r4_css`, `test_site_r4_emulators`, `test_site_r4_entry`, `test_site_r4_merge`, `test_site_r4_p0`, `test_site_r4_reference`, `test_site_r4_search`, `test_site_r4_systems`, `test_site_r4_wiki`, `test_site_r5_components`, `test_site_r5_css`, `test_site_r5_emulators`, `test_site_r5_entry`, `test_site_r5_reference`, `test_site_r5_search`, `test_site_r5_systems`, `test_site_r5_wiki`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `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` |
## Test architecture
+2 -2
View File
@@ -1320,10 +1320,10 @@ Cross-platform BIOS installer for end users:
python install.py
# One-line automatic install (Linux/macOS)
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
curl -fsSL https://abdess.github.io/retrobios/i | sh
# One-line automatic install (Windows PowerShell)
irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex
irm https://abdess.github.io/retrobios/i.ps1 | iex
```
`install.py` auto-detects the user's platform by checking config files,
+2 -2
View File
@@ -22,7 +22,7 @@ missing**? Run the command with `--check`. It changes nothing.
<!-- os-tabs -->
```sh
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --check
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --check
```
Read the count line:
@@ -85,7 +85,7 @@ cites them. Steam Deck: the Flatpak build. -->
<!-- os-tabs windows: --platform retroarch --dest C:\RetroArch-Win64\system macos: --platform retroarch --dest ~/Documents/RetroArch/system steamdeck: --platform retroarch --dest ~/.var/app/org.libretro.RetroArch/config/retroarch/system -->
```sh
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest ~/.config/retroarch/system
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retroarch --dest ~/.config/retroarch/system
```
Other messages: [When something goes wrong](installer.md#when-something-goes-wrong).