mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 04:39:06 -05:00
feat: packs for the most downloaded setups
This commit is contained in:
commit
eeef4fc6fb
34 files changed
+1187
-253
No files matched your search
@@ -5,7 +5,9 @@ Run on the draft before publishing it, and on the published release at any
|
||||
time. Every pack asset release.json names must be on the release at the sha256
|
||||
SHA256SUMS.txt lists for it, as must each handheld launcher (RetroBIOS.zip
|
||||
and the others build_port.py writes) the list names, the parts of a pack must add up to the download
|
||||
size the README prints, and nothing else may be attached. An upload that
|
||||
size the README prints, and nothing else may be attached. The record holds
|
||||
one pack for each platform the registry marks release_pack, and no other
|
||||
pack. An upload that
|
||||
failed in the loop, or a part re-uploaded from an older build, otherwise
|
||||
publishes a release that `sha256sum --check` rejects for every user.
|
||||
|
||||
@@ -29,6 +31,7 @@ from pathlib import Path
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from build_port import ARCHIVES
|
||||
from check_release_assets import digests_of
|
||||
from common import list_release_pack_platforms
|
||||
from largefiles import LARGE_FILES_REPO
|
||||
from release_record import RECORD, load_record
|
||||
|
||||
@@ -69,15 +72,31 @@ def compare_release(
|
||||
assets: dict[str, Asset],
|
||||
tag: str,
|
||||
local_files: dict[str, str] | None = None,
|
||||
released: set[str] | None = None,
|
||||
) -> list[Finding]:
|
||||
"""Every way the release can differ from what was built.
|
||||
|
||||
*local_files* maps a file kept beside the packs (the checksum list, its
|
||||
signature) to its local sha256: the copy on the release must be that one.
|
||||
*released* names the platforms the registry marks release_pack: each one
|
||||
has a recorded pack, and no recorded pack serves only other platforms.
|
||||
"""
|
||||
findings: list[Finding] = []
|
||||
if record.get("tag") != tag:
|
||||
findings.append(Finding("tag", tag, f"release.json records {record.get('tag')}"))
|
||||
if released is not None:
|
||||
packs = record.get("packs", {})
|
||||
served = {p for pack in packs.values() for p in pack.get("platforms", ())}
|
||||
for platform in sorted(released - served):
|
||||
findings.append(Finding(
|
||||
"no-pack", platform, "release_pack: true, no pack in release.json"
|
||||
))
|
||||
for pack_name, pack in sorted(packs.items()):
|
||||
if not set(pack.get("platforms", ())) & released:
|
||||
findings.append(Finding(
|
||||
"not-released", pack_name,
|
||||
"serves no platform marked release_pack in platforms/_registry.yml",
|
||||
))
|
||||
|
||||
recorded = {
|
||||
name: pack_name
|
||||
@@ -167,6 +186,10 @@ def main() -> int:
|
||||
help=f"local {SUMS} the packs were listed in. Its copy and signature on "
|
||||
f"the release must be these files (default: the list attached to the release)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--platforms-dir", default="platforms",
|
||||
help="registry whose release_pack platforms each need a pack (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="JSON output")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -199,7 +222,8 @@ def main() -> int:
|
||||
print(f"ERROR: {SUMS}: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
findings = compare_release(record, sums, assets, tag, local_files)
|
||||
released = set(list_release_pack_platforms(args.platforms_dir, include_archived=True))
|
||||
findings = compare_release(record, sums, assets, tag, local_files, released)
|
||||
if args.json:
|
||||
print(json.dumps(
|
||||
{"tag": tag, "draft": draft, "assets": len(assets),
|
||||
|
||||
@@ -437,6 +437,25 @@ def list_registered_platforms(
|
||||
return platforms
|
||||
|
||||
|
||||
def list_release_pack_platforms(
|
||||
platforms_dir: str = "platforms",
|
||||
include_archived: bool = False,
|
||||
) -> list[str]:
|
||||
"""Registered platforms a release publishes a ZIP pack for.
|
||||
|
||||
`release_pack: true` in _registry.yml is the whole list: the build of
|
||||
`--all`, the pack checks, the release record and every download table
|
||||
read it. The other platforms keep their install manifest and are served
|
||||
by the installer.
|
||||
"""
|
||||
registry = load_platform_registry(platforms_dir)
|
||||
return [
|
||||
name
|
||||
for name in list_registered_platforms(platforms_dir, include_archived)
|
||||
if registry.get(name, {}).get("release_pack") is True
|
||||
]
|
||||
|
||||
|
||||
# A list this short is printed whole rather than pointed at.
|
||||
SHORT_LIST = 12
|
||||
|
||||
|
||||
@@ -85,6 +85,15 @@ class Pack:
|
||||
expected_parts: int = 1
|
||||
|
||||
|
||||
# A release publishes packs for the setups most people download. The others
|
||||
# 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>"
|
||||
)
|
||||
|
||||
|
||||
def get_latest_release() -> dict:
|
||||
"""Fetch latest release info from GitHub API."""
|
||||
url = f"{API}/repos/{REPO}/releases/latest"
|
||||
@@ -319,6 +328,7 @@ def show_info(platform: str, release: dict) -> None:
|
||||
if not pack:
|
||||
print(f"Platform '{platform}' not found in release.")
|
||||
print("Available:", ", ".join(list_platforms(release)))
|
||||
print(NO_PACK_HINT)
|
||||
sys.exit(1)
|
||||
|
||||
print(f" Platform: {pack.platform}")
|
||||
@@ -389,6 +399,7 @@ To check files already in place, use: python install.py --check
|
||||
if not pack:
|
||||
print(f"Platform '{args.platform}' not found in release.")
|
||||
print("Available:", ", ".join(list_platforms(release)))
|
||||
print(NO_PACK_HINT)
|
||||
sys.exit(1)
|
||||
|
||||
dest = Path(os.path.expanduser(args.dest))
|
||||
|
||||
@@ -115,6 +115,8 @@ EOF
|
||||
echo "Error: Platform '$platform' not found in latest release."
|
||||
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>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+29
-14
@@ -57,6 +57,7 @@ from common import (
|
||||
canonical_target_name,
|
||||
platform_files_target,
|
||||
list_registered_platforms,
|
||||
list_release_pack_platforms,
|
||||
list_system_ids,
|
||||
load_data_dir_registry,
|
||||
load_database,
|
||||
@@ -2623,7 +2624,9 @@ def _run_verify_packs(args: argparse.Namespace) -> None:
|
||||
if args.platform:
|
||||
platforms = [args.platform]
|
||||
elif args.all:
|
||||
platforms = list_registered_platforms(
|
||||
# The packs --all builds: a platform without release_pack has no pack
|
||||
# to check, and its absence from the directory is no failure.
|
||||
platforms = list_release_pack_platforms(
|
||||
args.platforms_dir, include_archived=args.include_archived
|
||||
)
|
||||
else:
|
||||
@@ -3112,7 +3115,9 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
help="Platform name (e.g., retroarch)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all", action="store_true", help="Generate packs for all active platforms"
|
||||
"--all", action="store_true",
|
||||
help="Generate the release packs (release_pack in the registry), or with"
|
||||
" --manifest the install manifests of every platform",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--emulator", "-e", help="Emulator profile name(s), comma-separated"
|
||||
@@ -3264,10 +3269,14 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
)
|
||||
return
|
||||
if args.list:
|
||||
# Archived platforms build like the others but have no release pack.
|
||||
# Every platform builds with --platform. --all builds the release
|
||||
# packs only, the others are served by the installer.
|
||||
active = set(list_registered_platforms(args.platforms_dir))
|
||||
released = set(list_release_pack_platforms(args.platforms_dir, include_archived=True))
|
||||
for p in list_platforms(args.platforms_dir):
|
||||
print(p if p in active else f"{p} (archived)")
|
||||
marks = [m for m, on in (("archived", p not in active),
|
||||
("installer only", p not in released)) if on]
|
||||
print(f"{p} ({', '.join(marks)})" if marks else p)
|
||||
return
|
||||
if args.list_emulators:
|
||||
list_emulator_profiles(args.emulators_dir)
|
||||
@@ -3393,15 +3402,20 @@ def _main(held: contextlib.ExitStack) -> None:
|
||||
system_filter = getattr(args, "system_ids", None)
|
||||
|
||||
# Platform mode
|
||||
if args.all:
|
||||
# Install manifests cover every registered platform. "Archived" means
|
||||
if args.all and args.manifest:
|
||||
# Install manifests cover every registered platform: the installer
|
||||
# serves each one, with or without a release pack. "Archived" means
|
||||
# upstream is no longer scraped on a schedule, not that the platform
|
||||
# lost its users: its packs still ship, so the installer still needs
|
||||
# its file list.
|
||||
platforms = list_registered_platforms(
|
||||
args.platforms_dir,
|
||||
include_archived=args.include_archived or args.manifest,
|
||||
# lost its users.
|
||||
platforms = list_registered_platforms(args.platforms_dir, include_archived=True)
|
||||
elif args.all:
|
||||
# A release publishes the packs the registry marks release_pack and
|
||||
# nothing else: a pack built for another platform is one nobody uploads.
|
||||
platforms = list_release_pack_platforms(
|
||||
args.platforms_dir, include_archived=args.include_archived
|
||||
)
|
||||
if not platforms:
|
||||
parser.error("no registered platform has release_pack: true")
|
||||
elif args.platform:
|
||||
platforms = [args.platform]
|
||||
else:
|
||||
@@ -4816,10 +4830,11 @@ def _expected_pack_names(
|
||||
}
|
||||
for p in platforms
|
||||
}
|
||||
# Groups depend on which platforms a run built: --all alone or with
|
||||
# --include-archived. Both compositions are names a pack can carry.
|
||||
# Groups depend on which platforms a run built: the release packs of
|
||||
# --all, alone or with --include-archived. Both compositions are names a
|
||||
# pack can carry.
|
||||
for include_archived in (False, True):
|
||||
pool = list_registered_platforms(
|
||||
pool = list_release_pack_platforms(
|
||||
platforms_dir, include_archived=include_archived
|
||||
)
|
||||
cache = None
|
||||
|
||||
+68
-23
@@ -229,22 +229,23 @@ def release_totals(platform_name: str, record: dict) -> tuple[int | None, int |
|
||||
return pack["files"], pack["extracted_size"]
|
||||
|
||||
|
||||
def download_links(
|
||||
NO_PACK = "Use the command"
|
||||
|
||||
|
||||
def asset_links(
|
||||
platform_name: str, record: dict, repo: str, label: str, attrs: str = "",
|
||||
button: Callable[[str, str], str] | None = None,
|
||||
) -> str:
|
||||
"""Markdown links to the files of a platform's pack in the recorded release.
|
||||
|
||||
A pack published whole gets one link labelled *label*. A pack in parts
|
||||
gets one link per part, since every part is needed, after the number of
|
||||
files to download: a reader who took one link for the pack extracted
|
||||
half of it. *repo* is the
|
||||
repository URL, absolute or relative, and *attrs* an attribute list
|
||||
put after each link, or *button* the function that draws each link as a
|
||||
button on the site. Without a recorded release the link is the latest
|
||||
release page. A release recorded without this platform's pack says so and
|
||||
names the installer, which reads the file lists on main: a button to the
|
||||
release served six platforms a page with no pack for them.
|
||||
Written on the platform's get/ page only: every other surface links that
|
||||
page, whose address does not change with the release. A pack published
|
||||
whole gets one link labelled *label*. A pack in parts gets one link per
|
||||
part, since every part is needed, after the number of files to download:
|
||||
a reader who took one link for the pack extracted half of it. *repo* is
|
||||
the repository URL, *attrs* an attribute list put after each link, or
|
||||
*button* the function that draws each link as a button. Without a
|
||||
recorded release the link is the latest release page.
|
||||
"""
|
||||
pack = release_record.pack_for(platform_name, record)
|
||||
tag = record.get("tag")
|
||||
@@ -252,10 +253,8 @@ def download_links(
|
||||
def link(text: str, url: str) -> str:
|
||||
return button(text, url) if button else f"[{text}]({url}){attrs}"
|
||||
|
||||
if not tag:
|
||||
if not tag or not assets:
|
||||
return link(label, f"{repo}/releases/latest")
|
||||
if not assets:
|
||||
return f"Not in {tag}: [use the installer]({SITE_URL}wiki/installer/)"
|
||||
urls = [f"{repo}/releases/download/{tag}/{asset}" for asset in assets]
|
||||
if len(urls) == 1:
|
||||
return link(label, urls[0])
|
||||
@@ -269,6 +268,34 @@ def download_links(
|
||||
return f"{label}, {len(urls)} files, all needed: " + ", ".join(parts)
|
||||
|
||||
|
||||
def download_links(
|
||||
platform_name: str, record: dict, label: str, attrs: str = "",
|
||||
button: Callable[[str, str], str] | None = None,
|
||||
site: str = "", platforms_dir: str = "platforms", page: str | None = None,
|
||||
) -> str:
|
||||
"""The download cell of a platform: its permanent get/ page.
|
||||
|
||||
*record* is the release record reduced to the release_pack platforms
|
||||
(release_record.advertised). A platform with a pack in it gets *label*
|
||||
linked to its page, which lists the files of the current release. A
|
||||
platform marked release_pack but missing from the release, added since,
|
||||
gets "No ZIP yet". Any other platform is served by the installer, and
|
||||
the cell says so instead of staying empty. *site* is the site root the
|
||||
get/ address hangs from, or *page* the address itself as the site links
|
||||
it, and *attrs* and *button* style the link.
|
||||
"""
|
||||
page = page or release_record.get_page(platform_name, site)
|
||||
def link(text: str) -> str:
|
||||
return button(text, page) if button else f"[{text}]({page}){attrs}"
|
||||
|
||||
if (load_platform_registry(platforms_dir).get(platform_name) or {}).get("release_pack") is not True:
|
||||
return link(NO_PACK)
|
||||
pack = release_record.pack_for(platform_name, record)
|
||||
if record.get("tag") and not (pack and pack.get("assets")):
|
||||
return f"No ZIP yet: {link(NO_PACK.lower())}"
|
||||
return link(label)
|
||||
|
||||
|
||||
def extract_targets(platforms_dir: str) -> list[tuple[str, str]]:
|
||||
"""(display name, folder) per registered platform, where its pack extracts.
|
||||
|
||||
@@ -296,11 +323,17 @@ def own_root(config: dict) -> str:
|
||||
return tops.pop() if len(tops) == 1 else "bios"
|
||||
|
||||
|
||||
def extract_notes(platforms_dir: str) -> list[str]:
|
||||
"""A pack whose entries carry their own root extracts above the BIOS folder."""
|
||||
def extract_notes(platforms_dir: str, released_only: bool = False) -> list[str]:
|
||||
"""A pack whose entries carry their own root extracts above the BIOS folder.
|
||||
|
||||
*released_only* keeps the platforms a release publishes a pack for.
|
||||
"""
|
||||
folders = dict(extract_targets(platforms_dir))
|
||||
registry = load_platform_registry(platforms_dir)
|
||||
notes = []
|
||||
for key in sorted(load_platform_registry(platforms_dir)):
|
||||
for key in sorted(registry):
|
||||
if released_only and registry[key].get("release_pack") is not True:
|
||||
continue
|
||||
config = load_platform_config(key, platforms_dir)
|
||||
display = config.get("platform", key)
|
||||
if config.get("base_destination") == "" and display in folders:
|
||||
@@ -340,11 +373,21 @@ def download_table(
|
||||
f"| {display} | {f'{files:,}' if files else '-'} |"
|
||||
f" {format_size(size) if size else '-'} |"
|
||||
f" {extract_paths.get(cov['platform'], '')} |"
|
||||
f" {download_links(name, record, REPO_PATH, 'Download')} |"
|
||||
f" {download_links(name, record, 'Download', site=SITE_URL)} |"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def no_pack_line() -> str:
|
||||
"""Why some rows read "Use the command", in one sentence."""
|
||||
return (
|
||||
"ZIPs are made for the setups most people download. The others read"
|
||||
f" \"{NO_PACK}\": the installer above installs every file for them and"
|
||||
" checks each one, and `--dry-run` shows what it would install"
|
||||
f" ([why no ZIP]({SITE_URL}wiki/faq/#why-is-there-no-zip-for-my-setup))."
|
||||
)
|
||||
|
||||
|
||||
def _names_list(coverages: dict) -> str:
|
||||
"""The platforms' names in reading order, the last after "and"."""
|
||||
names = sorted(c["platform"] for c in coverages.values())
|
||||
@@ -500,7 +543,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
'<img src="https://github.com/Abdess/retrobios/actions/workflows/deploy-site.yml/badge.svg" alt="Site"></a>',
|
||||
"</p>",
|
||||
"",
|
||||
f"BIOS and firmware files for {_names_list(coverages)}, one pack per platform.",
|
||||
f"BIOS and firmware files for {_names_list(coverages)}.",
|
||||
"",
|
||||
"A pack holds the files its emulators read, as their source code names"
|
||||
" them, and leaves out what the emulators install themselves.",
|
||||
@@ -532,7 +575,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
"",
|
||||
"## Download BIOS packs",
|
||||
"",
|
||||
"One pack per platform, and it holds everything the platform runs: its own BIOS list plus every file its emulator cores load. Pick your platform, download the ZIP, extract to the BIOS path. The installer above does the same file by file. At a terminal it asks whether to install everything or to choose systems, cores and regions, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.",
|
||||
"A pack holds everything its platform runs: its own BIOS list plus every file its emulator cores load. Pick your platform, download the ZIP, extract to the BIOS path. Each Download link is a page that keeps its address from one release to the next. The installer above does the same file by file. At a terminal it asks whether to install everything or to choose systems, cores and regions, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.",
|
||||
"",
|
||||
"A pack over 2 GB comes in several parts (`Pack.part1of2.zip`,"
|
||||
" `Pack.part2of2.zip`), and every part is needed. Each part is an"
|
||||
@@ -564,16 +607,18 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
|
||||
lines.extend(
|
||||
download_table(
|
||||
coverages, archived, extract_paths, release_record.load_record()
|
||||
coverages, archived, extract_paths,
|
||||
release_record.load_advertised(platforms_dir=platforms_dir),
|
||||
)
|
||||
)
|
||||
lines.extend(["", no_pack_line()])
|
||||
|
||||
lines.extend(_paragraphs(extract_notes(platforms_dir)))
|
||||
lines.extend(_paragraphs(extract_notes(platforms_dir, released_only=True)))
|
||||
if archived:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"\\* Archived: the configuration is kept and packs are still built,"
|
||||
"\\* Archived: the configuration is kept and the installer serves it,"
|
||||
" but upstream is no longer scraped on a schedule.",
|
||||
]
|
||||
)
|
||||
|
||||
+142
-27
@@ -70,6 +70,8 @@ yaml = require_yaml()
|
||||
from generate_readme import (
|
||||
compute_coverage,
|
||||
platform_coverages,
|
||||
NO_PACK,
|
||||
asset_links,
|
||||
contributing_text,
|
||||
download_links,
|
||||
extract_notes,
|
||||
@@ -115,7 +117,8 @@ RELEASE_URL = f"{REPO_URL}/releases/latest"
|
||||
# metadata. The BIOS files themselves are under NOTICE, never this.
|
||||
DATA_LICENSE = "MIT (LICENSE: scripts, tooling, schemas, documentation and metadata)"
|
||||
SITE_URL = "https://abdess.github.io/retrobios/"
|
||||
GENERATED_DIRS = ["platforms", "systems", "emulators", "wiki", "api", "downloads"]
|
||||
GENERATED_DIRS = ["platforms", "systems", "emulators", "wiki", "api", "downloads",
|
||||
release_record.GET_DIR]
|
||||
# Generated directories of files served as they are, not pages.
|
||||
PUBLIC_DIRS = ("api", "downloads")
|
||||
|
||||
@@ -696,24 +699,115 @@ def version_line(platform_name: str, record: dict, named: bool = False) -> str:
|
||||
return f"Made for {made_from}. {OTHER_VERSION}"
|
||||
|
||||
|
||||
def _pack_buttons(platform_name: str, label: str = "Pack") -> str:
|
||||
"""Buttons to the files of the platform's pack in the recorded release,
|
||||
the release they come from, and the setup version its list was read at.
|
||||
"Does this pack fit my version?" is the first question a frontend user
|
||||
asks."""
|
||||
record = release_record.load_record()
|
||||
# Why a setup has no ZIP, answered once on the FAQ.
|
||||
NO_ZIP_FAQ = "wiki/faq.md#why-is-there-no-zip-for-my-setup"
|
||||
|
||||
|
||||
def has_release_pack(platform_name: str) -> bool:
|
||||
"""Whether a release publishes a ZIP for the platform (`release_pack`)."""
|
||||
entry = load_platform_registry("platforms").get(platform_name) or {}
|
||||
return entry.get("release_pack") is True
|
||||
|
||||
|
||||
def no_zip_line(display: str) -> str:
|
||||
"""The one line a setup without a release pack shows where the ZIP would be."""
|
||||
return f"No ZIP for {display}. The command installs every file and checks it."
|
||||
|
||||
|
||||
def get_page_link(platform_name: str, prefix: str = "") -> str:
|
||||
"""The platform's permanent download page, as a site link."""
|
||||
return f"{prefix}{release_record.GET_DIR}/{platform_name}.md"
|
||||
|
||||
|
||||
def _pack_buttons(platform_name: str, label: str = "Pack", prefix: str = "../",
|
||||
display: str | None = None) -> str:
|
||||
"""The way to the platform's ZIP from its setup page: a button to its
|
||||
get/ page, the release it comes from, and the setup version its list
|
||||
was read at. "Does this pack fit my version?" is the first question a
|
||||
frontend user asks. A setup with no release pack gets the line that
|
||||
says so, and why."""
|
||||
if not has_release_pack(platform_name):
|
||||
display = display or load_platform_config(platform_name, "platforms").get(
|
||||
"platform", platform_name)
|
||||
return f"{no_zip_line(display)} [Why no ZIP?]({prefix}{NO_ZIP_FAQ})"
|
||||
record = release_record.load_advertised()
|
||||
buttons = download_links(
|
||||
platform_name,
|
||||
record,
|
||||
REPO_URL,
|
||||
label,
|
||||
button=lambda text, url: markup.button(text, url, primary=True),
|
||||
page=get_page_link(platform_name, prefix),
|
||||
)
|
||||
released = release_line(platform_name, record)
|
||||
return "\n\n".join([buttons, *released,
|
||||
version_line(platform_name, record, named=len(released) > 1)])
|
||||
|
||||
|
||||
def generate_get_page(platform_name: str) -> str:
|
||||
"""get/<platform>/: the address a link outside the site can keep.
|
||||
|
||||
Every release deletes the previous tag, and with it each asset URL: six
|
||||
of the eight links a wiki had to our packs went dead. This page lists
|
||||
the files of the pack the current release holds, read from release.json,
|
||||
or the command when no release publishes one. The release process
|
||||
never touches it: committing release.json rebuilds it."""
|
||||
config = load_platform_config(platform_name, "platforms")
|
||||
display = config.get("platform", platform_name)
|
||||
record = release_record.load_advertised()
|
||||
pack = release_record.pack_for(platform_name, record)
|
||||
released = has_release_pack(platform_name)
|
||||
lines = ["---", "search: exclude", "---", "", f"# Download {display}", ""]
|
||||
if released and pack and pack.get("assets"):
|
||||
parts = len(pack["assets"])
|
||||
release = release_line(platform_name, record)
|
||||
extract_to = (load_platform_registry("platforms").get(platform_name) or {}).get("extract_to")
|
||||
lines += [
|
||||
"This address stays the same. It always gives the latest ZIP.",
|
||||
"",
|
||||
asset_links(platform_name, record, REPO_URL, f"Download {display} Pack",
|
||||
button=lambda text, url: markup.button(text, url, primary=True)),
|
||||
"",
|
||||
f"- {_fmt_size(pack['download_size'])} to download, "
|
||||
f"{_fmt_size(pack['extracted_size'])} once unzipped.",
|
||||
*(["- You need both parts." if parts == 2 else f"- You need all {parts} parts."]
|
||||
if parts > 1 else []),
|
||||
f"- Unzip {'them all' if parts > 1 else 'it'} into "
|
||||
+ (f"`{extract_to}`." if extract_to else "your BIOS folder."),
|
||||
"",
|
||||
*(line for text in release for line in (text, "")),
|
||||
version_line(platform_name, record, named=len(release) > 1),
|
||||
"",
|
||||
]
|
||||
elif released and record.get("tag"):
|
||||
lines += [
|
||||
f"No {display} ZIP yet. The next release has one. "
|
||||
"The command installs every file and checks it.",
|
||||
"",
|
||||
*install_commands(f"--platform {platform_name}"),
|
||||
]
|
||||
elif released:
|
||||
lines += [
|
||||
"This address stays the same. It always gives the latest ZIP.",
|
||||
"",
|
||||
markup.button(f"Download {display} Pack", RELEASE_URL, primary=True),
|
||||
"",
|
||||
]
|
||||
else:
|
||||
lines += [
|
||||
no_zip_line(display),
|
||||
"",
|
||||
*install_commands(f"--platform {platform_name}"),
|
||||
f"[Why no ZIP?](../{NO_ZIP_FAQ})",
|
||||
"",
|
||||
]
|
||||
lines += _next_block([
|
||||
f"[{display}](../platforms/{platform_name}.md#install): the folder, "
|
||||
"and how to run the command on the device",
|
||||
"[All installer options](../wiki/installer.md)",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# The home tile counts the whole collection. Labelled "Files" above a table
|
||||
# of pack buttons, it read as what one pack contains.
|
||||
COLLECTION_FILES_LABEL = "Files collected"
|
||||
@@ -2066,6 +2160,9 @@ def _page_description(markdown: str, relative: Path, title: str) -> str:
|
||||
if key in PART_PAGES:
|
||||
return (f"{title}: every file with its hashes, size, source references "
|
||||
"and who reads it.")
|
||||
if key.startswith(f"{release_record.GET_DIR}/"):
|
||||
return (f"{title}: the BIOS files of this setup, as the ZIP of the latest "
|
||||
"release or one command, at an address that stays the same.")
|
||||
if key.startswith("platforms/") and relative.stem != "index":
|
||||
return (f"{title}: where the BIOS files go, the command that installs them, the pack, "
|
||||
"and every file by system with its hashes.")
|
||||
@@ -2487,7 +2584,7 @@ def page_level(relative: Path) -> str:
|
||||
path = relative.as_posix()
|
||||
if path in ("index.md", "which-pack.md"):
|
||||
return "start"
|
||||
if relative.parts[0] == "platforms":
|
||||
if relative.parts[0] in ("platforms", release_record.GET_DIR):
|
||||
return "setup"
|
||||
if relative.parts[0] in ("systems", "emulators") or path == "explore.md":
|
||||
return "explore"
|
||||
@@ -2647,8 +2744,9 @@ def setup_table(coverages: dict, registry: dict | None = None) -> list[str]:
|
||||
status = registry.get(name, {}).get("status")
|
||||
archived |= status == "archived"
|
||||
listed = f"{cov['total']:,}" if cov["total"] else "No list of its own"
|
||||
files, _size = release_totals(name, release_record.load_record())
|
||||
zipped = f"{files:,}" if files else "No ZIP yet"
|
||||
files, _size = release_totals(name, release_record.load_advertised())
|
||||
zipped = (f"{files:,}" if files
|
||||
else "No ZIP yet" if has_release_pack(name) else "No ZIP")
|
||||
lines.append(
|
||||
f"| [{cov['platform']}](../platforms/{name}.md)"
|
||||
f"{' (archived)' if status == 'archived' else ''} | {listed} | {zipped} | "
|
||||
@@ -2659,8 +2757,10 @@ def setup_table(coverages: dict, registry: dict | None = None) -> list[str]:
|
||||
"The ZIP adds the files its emulators load to the files on the setup's own list.",
|
||||
"",
|
||||
]
|
||||
lines += ["No ZIP: the setup is served by the command alone. "
|
||||
f"[Why?](../{NO_ZIP_FAQ})", ""]
|
||||
if archived:
|
||||
lines += ["An archived setup keeps its page and its ZIP. Its list is no longer "
|
||||
lines += ["An archived setup keeps its page and the command. Its list is no longer "
|
||||
"read again on a schedule.", ""]
|
||||
return lines
|
||||
|
||||
@@ -3220,7 +3320,7 @@ def _computer_first(name: str, guide: Guide) -> bool:
|
||||
"""A system that runs only on its own device, with a ZIP to download: a
|
||||
beginner fills its folder from a computer more easily than from a
|
||||
terminal on the TV. Without a ZIP the command is the only route."""
|
||||
pack = release_record.pack_for(name, release_record.load_record())
|
||||
pack = release_record.pack_for(name, release_record.load_advertised())
|
||||
return guide.run == ("device",) and bool(pack and pack.get("assets"))
|
||||
|
||||
|
||||
@@ -3238,7 +3338,7 @@ def _guide_download(name: str, display: str, guide: Guide, extract_to: str | Non
|
||||
heading: str) -> list[str]:
|
||||
"""The pack: its buttons, its size, and where it is unzipped."""
|
||||
lines = [heading, ""]
|
||||
record = release_record.load_record()
|
||||
record = release_record.load_advertised()
|
||||
pack = release_record.pack_for(name, record)
|
||||
network = any(label.startswith("From ") for label, _path in guide.folders)
|
||||
if pack and pack.get("assets"):
|
||||
@@ -3263,6 +3363,8 @@ def _guide_download(name: str, display: str, guide: Guide, extract_to: str | Non
|
||||
*(f"- {b}" for b in bullets),
|
||||
"",
|
||||
]
|
||||
elif not has_release_pack(name):
|
||||
lines += [_pack_buttons(name, display=display), ""]
|
||||
elif record.get("tag"):
|
||||
made_from = setup_version(name)
|
||||
lines += [
|
||||
@@ -3386,12 +3488,13 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
|
||||
if guide.lead:
|
||||
lines += [guide.lead, ""]
|
||||
lines += _folder_rows(guide.folders)
|
||||
if extract_to:
|
||||
if extract_to and has_release_pack(name):
|
||||
lines.append(f"| Unzip the pack into | `{extract_to}` |")
|
||||
lines.append("")
|
||||
for note in guide.notes:
|
||||
lines += [note, ""]
|
||||
lines += [line for note in extract_notes("platforms") if f"The {display} pack " in note
|
||||
lines += [line for note in extract_notes("platforms", released_only=True)
|
||||
if f"The {display} pack " in note
|
||||
for line in (note, "")]
|
||||
if guide.more_folders:
|
||||
detected = _detected_systems(registry, name) if guide.more_folders_detected else []
|
||||
@@ -3573,7 +3676,7 @@ def generate_platform_page(
|
||||
detect = (registry.get(name, {}).get("install") or {}).get("detect") or []
|
||||
on_card = any(rule.get("method") == "card_marker" for rule in detect)
|
||||
lines.extend(["", *_platform_install_block(name, display, on_card)])
|
||||
lines.extend(["", _pack_buttons(name, f"Download {display} Pack")])
|
||||
lines.extend(["", _pack_buttons(name, f"Download {display} Pack", display=display)])
|
||||
lines.append("")
|
||||
|
||||
# The declaration behind each detail, by system and name in declaration
|
||||
@@ -8377,15 +8480,17 @@ def _folder_cell(name: str, guide: Guide, prefix: str = "") -> str:
|
||||
|
||||
def _pack_cell(name: str, record: dict, prefix: str = "") -> str:
|
||||
"""The pack's parts and size, or the command where the release has none."""
|
||||
if not has_release_pack(name):
|
||||
return f"No ZIP. [{NO_PACK}]({prefix}platforms/{name}.md#install)."
|
||||
pack = release_record.pack_for(name, record)
|
||||
if record.get("tag") and not (pack and pack.get("assets")):
|
||||
return f"No ZIP yet. [Use the command]({prefix}platforms/{name}.md#install)."
|
||||
# Small buttons, one per part, the size after the last: links wrapped
|
||||
# mid-label ("part 1 of / 3") in a narrow cell.
|
||||
links = download_links(name, record, REPO_URL, "ZIP",
|
||||
button=lambda text, url: markup.button(text, url))
|
||||
return f"No ZIP yet. [{NO_PACK}]({prefix}platforms/{name}.md#install)."
|
||||
# One small button to the setup's get/ page, which lists every part, and
|
||||
# the size after it.
|
||||
link = download_links(name, record, "ZIP", page=get_page_link(name, prefix),
|
||||
button=lambda text, url: markup.button(text, url))
|
||||
size = pack.get("download_size") if pack else None
|
||||
return f"{links} ({_fmt_size(size)})" if size else links
|
||||
return f"{link} ({_fmt_size(size)})" if size else link
|
||||
|
||||
|
||||
def _group_keys(registry: dict, group: str, display: dict[str, str]) -> list[str]:
|
||||
@@ -8462,7 +8567,7 @@ def generate_which_pack() -> str:
|
||||
The tiles are the choice. Open tables under them listed every setup a
|
||||
second time and made the page 11,800 px tall on a phone."""
|
||||
registry = load_platform_registry("platforms")
|
||||
record = release_record.load_record()
|
||||
record = release_record.load_advertised()
|
||||
display = {key: load_platform_config(key, "platforms").get("platform", key) for key in registry}
|
||||
all_tabs = ("windows", "macos", "linux", "steamdeck", "android")
|
||||
lines = [
|
||||
@@ -8485,10 +8590,12 @@ def generate_which_pack() -> str:
|
||||
*markup.details("note", "Every folder and ZIP in one table",
|
||||
_folder_tables(registry, record, display)),
|
||||
]
|
||||
no_zip = any(not (pack and pack.get("assets"))
|
||||
for pack in (release_record.pack_for(key, record) for key in registry))
|
||||
if record.get("tag") and no_zip:
|
||||
pending = any(has_release_pack(key) and not (pack and pack.get("assets"))
|
||||
for key, pack in ((key, release_record.pack_for(key, record)) for key in registry))
|
||||
if record.get("tag") and pending:
|
||||
lines += ["Setups added after the last release have no ZIP until the next one.", ""]
|
||||
lines += ["No ZIP on a row: the command serves that setup. ZIPs are made for the "
|
||||
f"setups most people download. [Why?]({NO_ZIP_FAQ})", ""]
|
||||
lines += [
|
||||
f"The ZIPs come from the [latest release]({RELEASE_URL}). The command may "
|
||||
"install newer files.",
|
||||
@@ -9127,6 +9234,14 @@ def _generate(args: argparse.Namespace) -> None:
|
||||
print("Generating which-pack page...")
|
||||
write_if_changed(str(docs / "which-pack.md"), generate_which_pack())
|
||||
|
||||
# One permanent download address per registered platform, archived ones
|
||||
# included: the README's table links each of them.
|
||||
print("Generating download pages...")
|
||||
get_dir = docs / release_record.GET_DIR
|
||||
get_dir.mkdir(parents=True, exist_ok=True)
|
||||
for name in platform_names:
|
||||
write_if_changed(str(get_dir / f"{name}.md"), generate_get_page(name))
|
||||
|
||||
print("Generating contributing page...")
|
||||
write_if_changed(str(docs / "contributing.md"), generate_contributing())
|
||||
|
||||
|
||||
+27
-3
@@ -317,8 +317,27 @@ def _match_key(label: str) -> set[str]:
|
||||
} - {""}
|
||||
|
||||
|
||||
def check_consistency(verify_output: str, pack_output: str) -> bool:
|
||||
"""Verify that check counts match between verify and pack for each platform."""
|
||||
def release_pack_keys(platforms_dir: str = "platforms", include_archived: bool = False) -> set[str]:
|
||||
"""Comparable identities of the platforms --all builds a pack for: their
|
||||
registry ids and display names, normalized as _match_key does."""
|
||||
from common import list_release_pack_platforms, load_platform_config # noqa: PLC0415
|
||||
|
||||
keys: set[str] = set()
|
||||
for name in list_release_pack_platforms(platforms_dir, include_archived):
|
||||
keys |= _match_key(name)
|
||||
keys |= _match_key(str(load_platform_config(name, platforms_dir).get("platform", name)))
|
||||
return keys
|
||||
|
||||
|
||||
def check_consistency(
|
||||
verify_output: str, pack_output: str, released: set[str] | None = None,
|
||||
) -> bool:
|
||||
"""Verify that check counts match between verify and pack for each platform.
|
||||
|
||||
*released* holds the identities of the platforms with a release pack
|
||||
(release_pack_keys). verify judges every platform, the build only those:
|
||||
a platform outside it has no pack to compare, which is no failure.
|
||||
"""
|
||||
v = parse_verify_counts(verify_output)
|
||||
p = parse_pack_counts(pack_output)
|
||||
excluded = parse_pack_exclusions(pack_output)
|
||||
@@ -377,6 +396,8 @@ def check_consistency(verify_output: str, pack_output: str) -> bool:
|
||||
f"({p_excluded} unsafe excluded) != verify {v_ok}/{v_total}"
|
||||
)
|
||||
all_ok = False
|
||||
elif released is not None and not (v_names & released):
|
||||
print(f" {v_label}: {v_ok}/{v_total} (no release pack, served by the installer)")
|
||||
else:
|
||||
print(f" {v_label}: {v_ok}/{v_total} (no separate pack)")
|
||||
all_ok = False
|
||||
@@ -1023,7 +1044,10 @@ def main() -> None:
|
||||
skip_notice("consistency", "no full pack built")
|
||||
results["consistency"] = SKIPPED
|
||||
elif pack_output and verify_output:
|
||||
ok = check_consistency(verify_output, pack_output)
|
||||
ok = check_consistency(
|
||||
verify_output, pack_output,
|
||||
release_pack_keys(include_archived=args.include_archived),
|
||||
)
|
||||
# Counts can agree while the paths differ, both ways at once.
|
||||
ok = check_pack_destinations(destinations_file, pack_output) and ok
|
||||
ok = check_emulator_packs() and ok
|
||||
|
||||
+58
-24
@@ -4,8 +4,10 @@
|
||||
The notes carried the pack table, typed by hand from figures release.json
|
||||
already held: a copied cell or an old collection total at the top is how
|
||||
issue 81 happened. Install commands, the parts sentence and the table come
|
||||
from the record and the registry. The maintainer writes what changed, and
|
||||
that text is appended as given.
|
||||
from the record and the registry. Each row links the platform's page under
|
||||
get/ on the site, which outlives the release, and the setups the registry
|
||||
does not mark release_pack are named with the command. The maintainer writes
|
||||
what changed, and that text is appended as given.
|
||||
|
||||
Usage:
|
||||
python scripts/release_notes.py --whats-new tmp/whats-new.md --output notes.md
|
||||
@@ -19,10 +21,17 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
import release_record
|
||||
from common import load_platform_registry, write_text_atomic
|
||||
from common import (
|
||||
list_registered_platforms,
|
||||
list_release_pack_platforms,
|
||||
load_platform_config,
|
||||
load_platform_registry,
|
||||
write_text_atomic,
|
||||
)
|
||||
|
||||
REPO = "Abdess/retrobios"
|
||||
RAW = f"https://raw.githubusercontent.com/{REPO}/main"
|
||||
SITE = "https://abdess.github.io/retrobios/"
|
||||
|
||||
|
||||
def _size(count: int) -> str:
|
||||
@@ -36,39 +45,63 @@ def _display(pack: str) -> str:
|
||||
return pack.split("_BIOS_Pack", 1)[0].replace("_", " ")
|
||||
|
||||
|
||||
def _owner(name: str, pack: dict) -> str:
|
||||
"""The platform a pack is named after: RetroArch_Lakka is RetroArch's."""
|
||||
key = _match_key(name)
|
||||
return sorted(
|
||||
pack.get("platforms", ()), key=lambda p: key.find(_match_key(p)) % (len(key) + 1)
|
||||
)[0]
|
||||
|
||||
|
||||
def _extract_folders(record: dict, platforms_dir: str) -> dict[str, str]:
|
||||
"""Pack name to the folder the registry says it extracts into."""
|
||||
registry = load_platform_registry(platforms_dir)
|
||||
folders: dict[str, str] = {}
|
||||
for name, pack in sorted(record.get("packs", {}).items()):
|
||||
# A shared pack extracts where the platform it is named after does:
|
||||
# RetroArch_Lakka is RetroArch's.
|
||||
key = _match_key(name)
|
||||
for platform in sorted(
|
||||
pack.get("platforms", ()), key=lambda p: key.find(_match_key(p)) % (len(key) + 1)
|
||||
):
|
||||
folder = registry.get(platform, {}).get("extract_to")
|
||||
if folder:
|
||||
folders[name] = folder
|
||||
break
|
||||
folder = registry.get(_owner(name, pack), {}).get("extract_to")
|
||||
if folder:
|
||||
folders[name] = folder
|
||||
return folders
|
||||
|
||||
|
||||
def without_pack(platforms_dir: str) -> list[str]:
|
||||
"""Display names of the registered setups no release publishes a ZIP for."""
|
||||
released = set(list_release_pack_platforms(platforms_dir, include_archived=True))
|
||||
names = [
|
||||
str(load_platform_config(key, platforms_dir).get("platform", key))
|
||||
for key in list_registered_platforms(platforms_dir, include_archived=True)
|
||||
if key not in released
|
||||
]
|
||||
return sorted(names, key=str.casefold)
|
||||
|
||||
|
||||
def no_pack_paragraph(platforms_dir: str) -> list[str]:
|
||||
"""One paragraph naming the setups served by the command alone."""
|
||||
names = without_pack(platforms_dir)
|
||||
if not names:
|
||||
return []
|
||||
listed = ", ".join(names[:-1]) + f" and {names[-1]}" if len(names) > 1 else names[0]
|
||||
return [
|
||||
(f"No ZIP for {listed}. ZIPs are made for the setups most people download."
|
||||
" The command above installs every file for these setups and checks each one."
|
||||
" Add `--dry-run` to see what it would install."),
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def _match_key(value: str) -> str:
|
||||
"""Letters and digits only: the platform is `misterfpga`, the pack MiSTer_FPGA."""
|
||||
return re.sub(r"[^a-z0-9]", "", value.lower())
|
||||
|
||||
|
||||
def _links(tag: str, assets: list[str]) -> str:
|
||||
base = f"https://github.com/{REPO}/releases/download/{tag}"
|
||||
if len(assets) == 1:
|
||||
return f"[{assets[0]}]({base}/{assets[0]})"
|
||||
parts = " + ".join(f"[part {n}]({base}/{asset})" for n, asset in enumerate(assets, 1))
|
||||
return f"{len(assets)} files, all needed: {parts}"
|
||||
def _link(name: str, pack: dict) -> str:
|
||||
"""The pack's permanent page, which lists the files of the current release."""
|
||||
parts = len(pack["assets"])
|
||||
label = "Download" if parts == 1 else f"Download, {parts} parts"
|
||||
return f"[{label}]({release_record.get_page(_owner(name, pack), SITE)})"
|
||||
|
||||
|
||||
def render(record: dict, platforms_dir: str, whats_new: str) -> str:
|
||||
tag = record["tag"]
|
||||
packs = record.get("packs", {})
|
||||
folders = _extract_folders(record, platforms_dir)
|
||||
lines = [
|
||||
@@ -89,8 +122,9 @@ def render(record: dict, platforms_dir: str, whats_new: str) -> str:
|
||||
"",
|
||||
"### Download a pack",
|
||||
"",
|
||||
("One pack per platform, the complete one: the BIOS list the platform"
|
||||
" publishes plus every file its emulators open. Files is what the pack"
|
||||
("One complete pack for each platform below: the BIOS list the platform"
|
||||
" publishes plus every file its emulators open. Each link is a page"
|
||||
" that keeps its address from one release to the next. Files is what the pack"
|
||||
" holds once extracted, the count a file manager shows. Extracted size"
|
||||
" is the space those files take, and the download is smaller."),
|
||||
"",
|
||||
@@ -109,11 +143,11 @@ def render(record: dict, platforms_dir: str, whats_new: str) -> str:
|
||||
for name, pack in sorted(packs.items(), key=lambda item: -item[1]["extracted_size"]):
|
||||
folder = folders.get(name)
|
||||
lines.append(
|
||||
f"| {_display(name)} | {_links(tag, pack['assets'])} |"
|
||||
f"| {_display(name)} | {_link(name, pack)} |"
|
||||
f" {_size(pack['extracted_size'])} | {pack['files']:,} |"
|
||||
f" {f'`{folder}`' if folder else '-'} |"
|
||||
)
|
||||
lines += ["", whats_new.strip(), ""]
|
||||
lines += ["", *no_pack_paragraph(platforms_dir), whats_new.strip(), ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
||||
@@ -26,11 +26,15 @@ import split_pack
|
||||
from common import (
|
||||
ArtifactLockBusy,
|
||||
artifact_lock,
|
||||
list_registered_platforms,
|
||||
list_release_pack_platforms,
|
||||
write_text_atomic,
|
||||
)
|
||||
|
||||
RECORD = "release.json"
|
||||
# Each registered platform has a page at get/<platform>/ on the site: the
|
||||
# current pack's files, or the command when no release publishes one. The
|
||||
# address outlives every release, so a link written elsewhere keeps working.
|
||||
GET_DIR = "get"
|
||||
|
||||
|
||||
class UnfinishedSplitError(Exception):
|
||||
@@ -46,11 +50,12 @@ def release_owners(platforms_dir: str = "platforms") -> dict[str, list[str]]:
|
||||
|
||||
Rebuilt from the name builder, never matched by substring: matched by
|
||||
substring, RetroArch_v1.22.2_AndroidArm64V8A_BIOS_Pack.zip sorts before
|
||||
the full pack and was taken for the RetroArch pack.
|
||||
the full pack and was taken for the RetroArch pack. Only the platforms
|
||||
marked release_pack own a release pack.
|
||||
"""
|
||||
import generate_pack
|
||||
|
||||
platforms = list_registered_platforms(platforms_dir, include_archived=True)
|
||||
platforms = list_release_pack_platforms(platforms_dir, include_archived=True)
|
||||
owners: dict[str, list[str]] = {}
|
||||
for platform, names in generate_pack._expected_pack_names(
|
||||
platforms, platforms_dir, None, None
|
||||
@@ -127,6 +132,35 @@ def load_record(path: str | Path = RECORD) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def get_page(platform: str, site: str = "") -> str:
|
||||
"""The permanent download address of a platform, under *site*."""
|
||||
return f"{site}{GET_DIR}/{platform}/"
|
||||
|
||||
|
||||
def advertised(record: dict, platforms_dir: str = "platforms") -> dict:
|
||||
"""The record as the download tables show it: release_pack platforms only.
|
||||
|
||||
A release cut before a platform lost its release_pack still holds that
|
||||
platform's pack. The tables follow the registry, so a platform left out
|
||||
is offered the command, and a pack two platforms shared names the one
|
||||
still released.
|
||||
"""
|
||||
if not record:
|
||||
return {}
|
||||
released = set(list_release_pack_platforms(platforms_dir, include_archived=True))
|
||||
packs = {}
|
||||
for name, pack in record.get("packs", {}).items():
|
||||
serves = [p for p in pack.get("platforms", ()) if p in released]
|
||||
if serves:
|
||||
packs[name] = {**pack, "platforms": serves}
|
||||
return {**record, "packs": packs}
|
||||
|
||||
|
||||
def load_advertised(path: str | Path = RECORD, platforms_dir: str = "platforms") -> dict:
|
||||
"""The committed record, reduced to the platforms a release publishes."""
|
||||
return advertised(load_record(path), platforms_dir)
|
||||
|
||||
|
||||
def pack_name_for(platform: str, record: dict) -> str | None:
|
||||
"""Name of the release pack that serves a platform, if it has one."""
|
||||
for name, pack in sorted(record.get("packs", {}).items()):
|
||||
|
||||
@@ -391,6 +391,31 @@ def _save_folder_once_paths(path: Path, document: dict) -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
def _registry_contract() -> list[str]:
|
||||
"""The registry against its schema, and a release that still ships a pack.
|
||||
|
||||
`release_pack` is the only list of the ZIPs a release publishes: a
|
||||
registry where no active platform carries it would cut a release with
|
||||
no pack at all, and every download link would lead to the command.
|
||||
"""
|
||||
registry_path = ROOT / "platforms" / "_registry.yml"
|
||||
if not registry_path.is_file():
|
||||
return []
|
||||
with registry_path.open(encoding="utf-8") as handle:
|
||||
registry = yaml_load(handle) or {}
|
||||
label = str(registry_path.relative_to(ROOT))
|
||||
out = _errors(_validator("registry.schema.json"), registry, label)
|
||||
platforms = registry.get("platforms") or {}
|
||||
if isinstance(platforms, dict) and not any(
|
||||
isinstance(entry, dict)
|
||||
and entry.get("release_pack") is True
|
||||
and entry.get("status", "active") == "active"
|
||||
for entry in platforms.values()
|
||||
):
|
||||
out.append(f"{label}: no active platform has release_pack: true")
|
||||
return out
|
||||
|
||||
|
||||
def _save_directory_blocks() -> list[str]:
|
||||
"""Each registry save_directory is null or a folder and a by_core flag.
|
||||
|
||||
@@ -572,6 +597,7 @@ def main() -> int:
|
||||
errors.extend(_unknown_data_dir_refs())
|
||||
errors.extend(_repository_layout_in_profiles())
|
||||
errors.extend(_save_directory_blocks())
|
||||
errors.extend(_registry_contract())
|
||||
|
||||
if not args.source_only:
|
||||
database_path = ROOT / "database.json"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
This complements Starlight's own link check (starlight-links-validator,
|
||||
which fails the build) with checks on the rendered HTML: metadata,
|
||||
heading structure, image alternatives, duplicate IDs, structured data, local
|
||||
resources, links and fragments, and the budgets of what a visitor downloads.
|
||||
resources, links and fragments, the budgets of what a visitor downloads, and
|
||||
a get/ download page for every registered platform.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -349,7 +350,54 @@ def _check_links(pages: dict, site: Path, base_path: str) -> list[str]:
|
||||
return issues
|
||||
|
||||
|
||||
def validate_site(site: Path, base_path: str = "/retrobios/") -> list[str]:
|
||||
RELEASE_DOWNLOAD = "https://github.com/Abdess/retrobios/releases/download/"
|
||||
|
||||
|
||||
def _check_get_pages(
|
||||
pages: dict, site: Path, platforms_dir: Path, record_path: Path,
|
||||
) -> list[str]:
|
||||
"""get/<platform>/ resolves for every registered platform, as a page.
|
||||
|
||||
Links outside the site keep these addresses across releases, so a
|
||||
missing one is a dead download link for whoever followed it. A platform
|
||||
with a pack in the release record links every one of its assets at the
|
||||
recorded tag, and one without links none and gives the command.
|
||||
"""
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import release_record # noqa: PLC0415
|
||||
from common import list_registered_platforms, list_release_pack_platforms # noqa: PLC0415
|
||||
|
||||
if not (platforms_dir / "_registry.yml").is_file():
|
||||
return [f"no platform registry in {platforms_dir}"]
|
||||
record = release_record.load_advertised(record_path, str(platforms_dir))
|
||||
released = set(list_release_pack_platforms(str(platforms_dir), include_archived=True))
|
||||
issues: list[str] = []
|
||||
for platform in list_registered_platforms(str(platforms_dir), include_archived=True):
|
||||
rel = f"{release_record.GET_DIR}/{platform}/index.html"
|
||||
page = pages.get((site / rel).resolve())
|
||||
if page is None or page.redirect:
|
||||
issues.append(f"{rel}: no download page for {platform}")
|
||||
continue
|
||||
assets = [link for link in page.links if link.startswith(RELEASE_DOWNLOAD)]
|
||||
pack = release_record.pack_for(platform, record)
|
||||
if platform in released and pack:
|
||||
expected = {f"{RELEASE_DOWNLOAD}{record['tag']}/{a}" for a in pack["assets"]}
|
||||
missing = sorted(expected - set(assets))
|
||||
if missing:
|
||||
issues.append(f"{rel}: does not link {', '.join(missing)}")
|
||||
elif platform not in released:
|
||||
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"):
|
||||
issues.append(f"{rel}: gives no install command")
|
||||
return issues
|
||||
|
||||
|
||||
def validate_site(
|
||||
site: Path, base_path: str = "/retrobios/", platforms_dir: Path | None = None,
|
||||
record_path: Path | None = None,
|
||||
) -> list[str]:
|
||||
site = site.resolve()
|
||||
if not site.is_dir():
|
||||
return [f"site directory does not exist: {site}"]
|
||||
@@ -376,6 +424,9 @@ def validate_site(site: Path, base_path: str = "/retrobios/") -> list[str]:
|
||||
|
||||
issues.extend(_check_links(pages, site, base_path))
|
||||
issues.extend(_check_pagefind(site))
|
||||
if platforms_dir is not None:
|
||||
issues.extend(_check_get_pages(
|
||||
pages, site, platforms_dir, record_path or platforms_dir.parent / "release.json"))
|
||||
|
||||
return issues
|
||||
|
||||
@@ -394,6 +445,14 @@ def main() -> None:
|
||||
"--base", default="/retrobios/",
|
||||
help="the path the site is served under (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--platforms-dir", default="platforms", type=Path,
|
||||
help="registry whose platforms each need a get/ page (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--record", default="release.json", type=Path,
|
||||
help="release record the get/ pages link (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-errors", type=int, default=100,
|
||||
help="stop listing after this many errors (default: %(default)s)",
|
||||
@@ -401,7 +460,7 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
issues = validate_site(Path(args.site_dir), args.base)
|
||||
issues = validate_site(Path(args.site_dir), args.base, args.platforms_dir, args.record)
|
||||
except (OSError, RuntimeError, ValueError) as exc:
|
||||
print(f"Site validation failed: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
Reference in new issue
Block a user