Choose the asset that matches the machine

Batocera runs on x86_64 mini-PCs, on the Raspberry Pi and on the ARM
handhelds alike. A cartridge is data for an emulator and runs anywhere,
which is why this store has been portable so far — but a native build
only starts on the architecture it was built for, and an x86_64 binary
installed on a Pi is worse than one never offered.

`kind` and `ext` may now be a map from architecture to value, with `*`
as a fallback; a plain string still means "the same everywhere", so
existing configs are untouched. The detected architecture is reported at
the top of `list` and as `detected_arch` in `config`, and a title with no
asset for the running machine is skipped with a reason that names it.

Verified on aarch64 against the live catalog: the cartridge config
behaves exactly as before, a bevy entry mapped per architecture pulls
bevydemo-1.0.0-linux-arm64.zip, and the binary inside is an AArch64 ELF.
A map without an entry for the machine skips and says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 07:55:17 +02:00
co-authored by Claude Opus 5
parent 18054331d9
commit b6d71aea1c
2 changed files with 75 additions and 6 deletions
+27 -1
View File
@@ -149,7 +149,7 @@ counts, favourites and scraped media, and two stores can share one gamelist.
| `catalog.statuses` | `["released", "archived"]` | catalog `status` values to install | | `catalog.statuses` | `["released", "archived"]` | catalog `status` values to install |
| `catalog.owner_id` | `null` | restrict to one publisher (`/api/software?owner_id=`) | | `catalog.owner_id` | `null` | restrict to one publisher (`/api/software?owner_id=`) |
| `catalog.only` / `catalog.exclude` | `[]` | software-name allow / deny lists | | `catalog.only` / `catalog.exclude` | `[]` | software-name allow / deny lists |
| `platforms` | c64, tic80 | platform → system, asset kind, extension, enabled | | `platforms` | c64, tic80 | platform → system, asset kind, extension, enabled; the kind may be per-architecture |
| `behavior.prune` | `true` | remove games that left the catalog or the filters | | `behavior.prune` | `true` | remove games that left the catalog or the filters |
| `behavior.timeout` | `30` | HTTP timeout, seconds | | `behavior.timeout` | `30` | HTTP timeout, seconds |
| `behavior.insecure` | `false` | skip TLS verification (self-hosted test instances) | | `behavior.insecure` | `false` | skip TLS verification (self-hosted test instances) |
@@ -172,6 +172,32 @@ launches, so they are not mapped by default. Adding one is a config edit:
"platforms": { "godot": { "system": "godot", "kind": "linux_x64", "ext": ".zip", "enabled": true } } "platforms": { "godot": { "system": "godot", "kind": "linux_x64", "ext": ".zip", "enabled": true } }
``` ```
### Per-architecture assets
Batocera runs on x86_64 mini-PCs, on the Raspberry Pi and on the ARM handhelds
alike, and a native binary only starts on the architecture it was built for —
an x86_64 build installs on a Pi and then does nothing. A cartridge is data for
an emulator, so it is architecture-independent; a native build is not.
Where the right asset depends on the machine, `kind` (and `ext`, if it differs)
can be a map instead of a string. `*` is the fallback:
```json
"platforms": {
"bevy": {
"system": "bevy",
"kind": { "x86_64": "linux_x64", "aarch64": "linux_arm64" },
"ext": ".zip",
"enabled": true
}
}
```
The engine reports the architecture it detected — `x86_64`, `aarch64`, `armhf`,
`x86` — at the top of `list`, and `config` prints it as `detected_arch`. With no
entry for the running machine and no `*`, the title is skipped and says so:
`skipped bevydemo: bevy has no asset kind for aarch64`.
A platform is also skipped when the box has no ROM folder for its system — A platform is also skipped when the box has no ROM folder for its system —
`list` reports that as `skipped <name>: no '<system>' ROM folder`. `list` reports that as `skipped <name>: no '<system>' ROM folder`.
+48 -5
View File
@@ -16,6 +16,7 @@ Standard library only — Batocera ships python3 but no pip packages.
import argparse import argparse
import json import json
import os import os
import platform
import shutil import shutil
import subprocess import subprocess
import sys import sys
@@ -281,6 +282,37 @@ def fetch_catalog(cfg, use_cache=False):
die(f"cannot fetch catalog from {url}: {exc}") die(f"cannot fetch catalog from {url}: {exc}")
def machine():
"""A gep architekturaja, `uname -m` szerint, normalizalt neven.
A gazdaplatform ugyanaz (Batocera), a gep viszont nem: x86_64 mini-PC,
Raspberry Pi es kezikonzol egyaránt. Egy x86_64 binaris ARM-on feltelepul,
de nem indul el, ezért a kiadasvalasztasnak tudnia kell, min fut.
"""
m = (platform.machine() or "").lower()
if m in ("x86_64", "amd64"):
return "x86_64"
if m in ("aarch64", "arm64"):
return "aarch64"
if m.startswith("armv") or m == "arm":
return "armhf"
if m in ("i386", "i486", "i586", "i686"):
return "x86"
return m or "unknown"
def resolve_for_arch(value, arch):
"""Egy config-erteket architekturara oldunk fel.
A `kind` es az `ext` lehet egyszeru string (minden gepen ugyanaz — igy
mukodik a cartridge), vagy architektura -> ertek leképezes. Az utóbbinal
a `"*"` kulcs szolgal tartaleknak.
"""
if not isinstance(value, dict):
return value
return value.get(arch, value.get("*"))
def asset_basename(path): def asset_basename(path):
"""`/file/blessingofra-2.0.0.prg` -> `blessingofra-2.0.0.prg`.""" """`/file/blessingofra-2.0.0.prg` -> `blessingofra-2.0.0.prg`."""
return os.path.basename(path.rstrip("/")) return os.path.basename(path.rstrip("/"))
@@ -317,8 +349,9 @@ def pick_release(entry, kind, ext):
return None, None return None, None
def select_games(cfg, catalog, available_systems): def select_games(cfg, catalog, available_systems, arch=None):
"""Catalog -> list of installable game dicts, plus a list of skip reasons.""" """Catalog -> list of installable game dicts, plus a list of skip reasons."""
arch = arch or machine()
statuses = {s.lower() for s in cfg["catalog"].get("statuses") or []} statuses = {s.lower() for s in cfg["catalog"].get("statuses") or []}
only = {n.lower() for n in cfg["catalog"].get("only") or []} only = {n.lower() for n in cfg["catalog"].get("only") or []}
exclude = {n.lower() for n in cfg["catalog"].get("exclude") or []} exclude = {n.lower() for n in cfg["catalog"].get("exclude") or []}
@@ -344,9 +377,15 @@ def select_games(cfg, catalog, available_systems):
if name.lower() in exclude: if name.lower() in exclude:
continue continue
release, file_name = pick_release(entry, spec["kind"], spec.get("ext", "")) kind = resolve_for_arch(spec["kind"], arch)
if not kind:
skipped.append(f"{name}: {sw.get('platform')} has no asset kind for {arch}")
continue
ext = resolve_for_arch(spec.get("ext", ""), arch) or ""
release, file_name = pick_release(entry, kind, ext)
if not release: if not release:
skipped.append(f"{name}: no '{spec['kind']}' asset in any release") skipped.append(f"{name}: no '{kind}' asset in any release")
continue continue
games.append( games.append(
@@ -793,7 +832,9 @@ def cmd_apply_gamelists(cfg, args):
def cmd_list(cfg, args): def cmd_list(cfg, args):
systems = detect_systems(cfg) systems = detect_systems(cfg)
catalog = fetch_catalog(cfg, use_cache=True) catalog = fetch_catalog(cfg, use_cache=True)
games, skipped = select_games(cfg, catalog, systems) arch = machine()
log(f"architecture: {arch}")
games, skipped = select_games(cfg, catalog, systems, arch)
installed = load_state()["installed"] installed = load_state()["installed"]
if not games: if not games:
@@ -844,7 +885,9 @@ def cmd_config(cfg, args):
write_json(args.config, DEFAULT_CONFIG) write_json(args.config, DEFAULT_CONFIG)
log(f"wrote default config to {args.config}") log(f"wrote default config to {args.config}")
return 0 return 0
print(json.dumps(cfg, indent=2, ensure_ascii=False)) # A felismert architektura nem a configbol jon, de az asset-valasztast
# eldonti, ezert itt is lathatonak kell lennie.
print(json.dumps({"detected_arch": machine(), **cfg}, indent=2, ensure_ascii=False))
return 0 return 0