chore: merge origin/main

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 14:02:45 +01:00
commit 025fd08a72
16 files changed
+484 -9

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Build the `retrobios` package PyPI serves: install.py as a command.
The wheel and the sdist hold install.py, byte for byte the copy the
bootstraps pin, a launcher that runs it as a script (`retrobios` and
`python -m retrobios`), the metadata, README.md and LICENSE. Nothing of the
collection. packaging/pypi/ holds the project file, the README and
`__main__.py`; this script stages them with install.py and LICENSE into a
build folder, writes `__init__.py` with the version, and runs `uv build`
there with the flit_core backend pinned in the project file.
The version is the UTC date of the last commit that changed install.py,
with the zeros PEP 440 drops left out: 2026-11-09 gives 2026.11.9. When
install.py changed several times that day, the newest gets .N, its rank in
the day: the third change gives 2026.11.9.3. The same install.py always
gives the same version, so PyPI refuses a republish of unchanged code.
SOURCE_DATE_EPOCH is that commit's time, so two builds of one commit give
the same bytes.
Usage:
python scripts/build_pypi.py
python scripts/build_pypi.py --output-dir tmp/pypi-dist
"""
from __future__ import annotations
import argparse
import datetime
import hashlib
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from sitebootstrap import PIN, SH_NAME
ROOT = Path(__file__).resolve().parent.parent
PACKAGE_DIR = ROOT / "packaging" / "pypi"
DEFAULT_OUTPUT = ROOT / "tmp" / "pypi-dist"
MODULE = "retrobios"
# The launcher. runpy runs install.py as __main__, so its own __main__ block
# keeps the exit codes it gives a reader that left (141) and Ctrl+C (130).
LAUNCHER = '''"""The RetroBIOS installer, install.py, run as the retrobios command."""
import runpy
from pathlib import Path
__version__ = "{version}"
def main() -> None:
runpy.run_path(str(Path(__file__).with_name("install.py")), run_name="__main__")
'''
def package_version(stamps: list[int]) -> str:
"""The version of install.py from its commit times, newest first (UTC)."""
if not stamps:
raise ValueError("install.py has no commit to date the package by")
day = datetime.datetime.fromtimestamp(stamps[0], datetime.timezone.utc).date()
rank = sum(1 for stamp in stamps
if datetime.datetime.fromtimestamp(stamp, datetime.timezone.utc).date() == day)
base = f"{day.year}.{day.month}.{day.day}"
return base if rank == 1 else f"{base}.{rank}"
def install_py_commits() -> list[int]:
"""Commit times of install.py, newest first, from git."""
out = subprocess.run(["git", "log", "--format=%ct", "--", "install.py"], cwd=ROOT,
capture_output=True, text=True, check=True).stdout
return [int(line) for line in out.split()]
def installer_bytes() -> bytes:
"""install.py, refused unless it is the one install.sh pins: the package
then runs the installer the one-line command runs, under the same id."""
data = (ROOT / "install.py").read_bytes()
pins = PIN[SH_NAME].findall((ROOT / "install.sh").read_text(encoding="utf-8"))
if pins != [hashlib.sha256(data).hexdigest()]:
raise ValueError("install.py is not the installer install.sh pins: recompute the pin")
return data
def stage(folder: Path, version: str) -> None:
"""The project as the backend reads it."""
module = folder / MODULE
module.mkdir(parents=True)
for name in ("pyproject.toml", "README.md"):
shutil.copyfile(PACKAGE_DIR / name, folder / name)
shutil.copyfile(ROOT / "LICENSE", folder / "LICENSE")
shutil.copyfile(PACKAGE_DIR / MODULE / "__main__.py", module / "__main__.py")
(module / "__init__.py").write_text(LAUNCHER.format(version=version), encoding="utf-8")
(module / "install.py").write_bytes(installer_bytes())
def build(output_dir: Path) -> tuple[Path, Path]:
"""The wheel and the sdist in output_dir, replacing earlier builds there."""
stamps = install_py_commits()
version = package_version(stamps)
output_dir.mkdir(parents=True, exist_ok=True)
for pattern in ("*.whl", "*.tar.gz"):
for old in output_dir.glob(f"{MODULE}-{pattern}"):
old.unlink()
scratch = ROOT / "tmp"
scratch.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix="pypi-stage-", dir=scratch) as name:
folder = Path(name) / "retrobios"
stage(folder, version)
# Without a flag, uv builds the sdist, then the wheel from that sdist:
# what a user building from the sdist gets is what is published.
subprocess.run(
["uv", "build", "--quiet", "--out-dir", str(output_dir.resolve()), str(folder)],
check=True, env={**os.environ, "SOURCE_DATE_EPOCH": str(stamps[0])})
wheel = output_dir / f"{MODULE}-{version}-py3-none-any.whl"
sdist = output_dir / f"{MODULE}-{version}.tar.gz"
for path in (wheel, sdist):
if not path.is_file():
raise RuntimeError(f"uv build wrote no {path.name}")
return wheel, sdist
def main() -> int:
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0])
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT,
help="where the wheel and the sdist go (default: tmp/pypi-dist)")
args = parser.parse_args()
for path in build(args.output_dir):
print(path)
return 0
if __name__ == "__main__":
from common import run_cli
run_cli(main)
+4 -1
View File
@@ -39,7 +39,7 @@ from common import (
)
from nativemode import check_label
import release_record
from siteguides import install_command
from siteguides import PACKAGE_COMMAND, install_command
from validate_pr import LARGE_FILE_ISSUE_URL
from largefiles import load_registered_paths
import pr_checks
@@ -649,6 +649,9 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
install_command("sh"),
"```",
"",
f"**With Python 3.8 or newer**: `{PACKAGE_COMMAND}` runs the same installer, and so"
" does `pipx run retrobios`.",
"",
f"**A handheld, an SD card, or not sure?** [Pick the device]({SITE_URL}which-pack/):"
" its page gives the folder and the command. A muOS, Onion, spruceOS, MinUI, KNULLI,"
" ROCKNIX, Lakka or MiSTer SD card plugged into the computer is found by itself.",
+6
View File
@@ -112,6 +112,7 @@ from siteguides import (
INSTALL_BASE,
INSTALL_PS1,
INSTALL_SH,
PACKAGE_COMMAND,
TERMINALS,
TRUST_ITEMS,
detected_systems as _detected_systems,
@@ -11951,6 +11952,11 @@ def generate_which_pack() -> str:
# handheld recording (sitesteps).
box_tabs = (*all_tabs, "device")
box_steps, box_demos = sitesteps.install_box(box_tabs)
# Python is on most Linux and macOS machines: the package runs the same
# installer there without the bootstrap.
for tab in ("macos", "linux"):
box_demos[tab] = [*box_demos[tab],
f"With Python 3.8 or newer, `{PACKAGE_COMMAND}` does the same.", ""]
lines = [
"---",
f"terms: {', '.join(IOS_TERMS)}",
+2
View File
@@ -393,6 +393,8 @@ 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}"
# The same installer from PyPI (scripts/build_pypi.py builds the package).
PACKAGE_COMMAND = "uvx retrobios"
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"]