mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
feat: serve the one-line installer from the site
The site serves install.sh at /retrobios/i and install.ps1 at /retrobios/i.ps1. generate_site.py writes both copies with the installer address set to install.py at the commit the site is built from (GITHUB_SHA in CI, HEAD locally), so the pinned SHA-256 and the downloaded installer always come from one commit, including while a deploy is still running. Every command a user copies names the short address. The raw form stays on the installer page as the fallback. Deploy Site now runs on changes to install.sh and install.ps1. The install box weight keeps the address length its ratio was fitted on, so no console page moves.
This commit is contained in:
1 parent
7c3e6ced09
commit
9e6af0aa6b
37 files changed
+486
-110
No files matched your search
+1
-2
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user