diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index cca04dae2..3bf76738b 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -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" diff --git a/README.md b/README.md index b81421389..90365bf8b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/install.ps1 b/install.ps1 index 0abf59682..0f4029b6e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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 } diff --git a/install.py b/install.py index a18ab87c5..877944c7a 100644 --- a/install.py +++ b/install.py @@ -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" diff --git a/install.sh b/install.sh index 8afd6013e..c71539c53 100755 --- a/install.sh +++ b/install.sh @@ -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() { diff --git a/install/launchers/index.json b/install/launchers/index.json index f095b7b4e..5b6389bf1 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -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 }, { diff --git a/platforms/_guides.yml b/platforms/_guides.yml index 189195290..d07af1e2c 100644 --- a/platforms/_guides.yml +++ b/platforms/_guides.yml @@ -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 diff --git a/scripts/download.py b/scripts/download.py index 5402965f8..a6234b659 100644 --- a/scripts/download.py +++ b/scripts/download.py @@ -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 " + " curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform " ) diff --git a/scripts/download.sh b/scripts/download.sh index 1140a9aa1..ce2ba8190 100755 --- a/scripts/download.sh +++ b/scripts/download.sh @@ -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 " + echo " curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform " exit 1 fi diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 4e3d48286..44bc9f558 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -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" diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 2e6af67d8..cd59777f4 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -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) diff --git a/scripts/packreadme.py b/scripts/packreadme.py index 2cdeb8ed3..ca80f9899 100644 --- a/scripts/packreadme.py +++ b/scripts/packreadme.py @@ -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.""" diff --git a/scripts/release_notes.py b/scripts/release_notes.py index 3661568df..3ff155eee 100644 --- a/scripts/release_notes.py +++ b/scripts/release_notes.py @@ -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", diff --git a/scripts/sitebootstrap.py b/scripts/sitebootstrap.py new file mode 100644 index 000000000..05d80e22f --- /dev/null +++ b/scripts/sitebootstrap.py @@ -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 diff --git a/scripts/sitediagrams.py b/scripts/sitediagrams.py index 2cba29ada..1d8b51835 100644 --- a/scripts/sitediagrams.py +++ b/scripts/sitediagrams.py @@ -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 "" diff --git a/scripts/siteguides.py b/scripts/siteguides.py index 46b59d436..0436ca768 100644 --- a/scripts/siteguides.py +++ b/scripts/siteguides.py @@ -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. diff --git a/scripts/validate_site.py b/scripts/validate_site.py index de39d443b..c922ad414 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -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 diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 7e068a04d..955b0dc52 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -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): diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index 415df4240..fd8742547 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -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") diff --git a/tests/test_doc_surfaces.py b/tests/test_doc_surfaces.py index 89055d9c0..6431b3afd 100644 --- a/tests/test_doc_surfaces.py +++ b/tests/test_doc_surfaces.py @@ -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")) diff --git a/tests/test_install.py b/tests/test_install.py index 0d3062fc2..5488c4fa9 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -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) diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 9494cc670..f5fd14ab5 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -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) diff --git a/tests/test_pack_readme.py b/tests/test_pack_readme.py index 14d3fea05..9218843ca 100644 --- a/tests/test_pack_readme.py +++ b/tests/test_pack_readme.py @@ -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__": diff --git a/tests/test_release_packs.py b/tests/test_release_packs.py index a403d2f7a..4e52ed197 100644 --- a/tests/test_release_packs.py +++ b/tests/test_release_packs.py @@ -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): diff --git a/tests/test_site_bootstrap.py b/tests/test_site_bootstrap.py new file mode 100644 index 000000000..4e295120f --- /dev/null +++ b/tests/test_site_bootstrap.py @@ -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 "], + "scripts/download.py": [f"curl -fsSL {INSTALL_SH} | sh -s -- --platform "], + } + 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() diff --git a/tests/test_site_density_cards.py b/tests/test_site_density_cards.py index afe64f11d..164ba30fe 100644 --- a/tests/test_site_density_cards.py +++ b/tests/test_site_density_cards.py @@ -207,10 +207,10 @@ class AnInstallBoxShowsOneCommand(unittest.TestCase): listed = self.text[self.text.index('
'):] 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'", listed) - self.assertIn("install.ps1))) --platform retrobat --system 'Neo Geo'", listed) + self.assertIn("/i | sh -s -- --platform retrobat --system 'Neo Geo'", listed) + self.assertIn("/i.ps1))) --platform retrobat --system 'Neo Geo'", listed) # The URL is no link inside the command, and the break adds nothing. - self.assertIn("https://raw.githubusercontent.com", listed) + self.assertIn("https://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("|", 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): diff --git a/tests/test_site_r2_wiki.py b/tests/test_site_r2_wiki.py index 173439b68..2c85a1f76 100644 --- a/tests/test_site_r2_wiki.py +++ b/tests/test_site_r2_wiki.py @@ -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 = ( "\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('', 1) diff --git a/tests/test_site_r5_wiki.py b/tests/test_site_r5_wiki.py index 2907adfab..5291fc4eb 100644 --- a/tests/test_site_r5_wiki.py +++ b/tests/test_site_r5_wiki.py @@ -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 = ("\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'', sitediagrams.expand_commands(self.PAGE)) diff --git a/website/src/components/FilePicker.astro b/website/src/components/FilePicker.astro index e37e7daf5..335bd11b1 100644 --- a/website/src/components/FilePicker.astro +++ b/website/src/components/FilePicker.astro @@ -68,7 +68,7 @@ const packs = Object.fromEntries( type Release = { tag: string; packs: Record }; - 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). diff --git a/wiki/faq.md b/wiki/faq.md index 30e3e567c..9c2268f73 100644 --- a/wiki/faq.md +++ b/wiki/faq.md @@ -20,7 +20,7 @@ terms: ios, iphone, ipad ```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: ```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 ``` diff --git a/wiki/first-install.md b/wiki/first-install.md index a342c1021..eb08de448 100644 --- a/wiki/first-install.md +++ b/wiki/first-install.md @@ -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 ``` @@ -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 ``` @@ -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 ``` @@ -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 ``` @@ -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 ``` @@ -120,7 +120,7 @@ Add `--check` to the same command. It changes nothing. ```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 ```sh -curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retropie --dest +curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retropie --dest ``` ### Error: the downloaded installer does not match its expected fingerprint. diff --git a/wiki/installer.md b/wiki/installer.md index d229bde5f..c8d4c7307 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -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 ``` diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 290932592..32525315a 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -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 diff --git a/wiki/tools.md b/wiki/tools.md index 12533ba86..1f3d16164 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -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, diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 6b39be823..9071ec742 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -22,7 +22,7 @@ missing**? Run the command with `--check`. It changes nothing. ```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. --> ```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).