Install native games as Ports, not ROMs

The store could only offer what an emulator boots, which meant cartridges
and nothing else — a little under half the catalog. Batocera runs Linux
and its Ports system takes .sh launchers, so a native build can be
installed too; it just is not a ROM.

`install: "port"` unpacks the zip, finds the executable and writes a
launcher that cd's into its directory first, because a game loads assets
relative to the working directory. Python's extraction drops the
executable bit, so it is restored from the mode the archive records.

The payload lives under ports/.data/<subfolder>/, whose leading dot is
what hides it from EmulationStation, while the launchers sit in a normal
subfolder that gets scanned like any other system's. Both carry the store
subfolder, so an uninstall or prune cannot reach another store's files —
verified by pointing a state entry at a foreign namespace and watching it
be refused.

Verified end to end on aarch64 against the live catalog: bevydemo
installs as a port with an executable launcher and binary, re-syncs
without re-downloading 29 MB, and uninstalls taking its 106 MB payload
with it. `install` defaults to rom, so existing configs are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 08:12:42 +02:00
co-authored by Claude Opus 5
parent b6d71aea1c
commit 4be089f9c3
3 changed files with 195 additions and 20 deletions
+40 -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.owner_id` | `null` | restrict to one publisher (`/api/software?owner_id=`) |
| `catalog.only` / `catalog.exclude` | `[]` | software-name allow / deny lists |
| `platforms` | c64, tic80 | platform → system, asset kind, extension, enabled; the kind may be per-architecture |
| `platforms` | c64, tic80 | platform → system, asset kind, extension, enabled; `install` picks ROM or Port, and the kind may be per-architecture |
| `behavior.prune` | `true` | remove games that left the catalog or the filters |
| `behavior.timeout` | `30` | HTTP timeout, seconds |
| `behavior.insecure` | `false` | skip TLS verification (self-hosted test instances) |
@@ -172,6 +172,45 @@ 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 } }
```
### Native games as Ports
A cartridge is booted by an emulator, so it goes into that system's ROM folder.
A native build is a program: Batocera runs Linux, and its **Ports** system takes
`.sh` launchers. Set `install` to `port` and point `system` at `ports`:
```json
"platforms": {
"bevy": {
"system": "ports",
"install": "port",
"kind": { "x86_64": "linux_x64", "aarch64": "linux_arm64" },
"ext": ".zip",
"enabled": true
}
}
```
The zip is unpacked and a launcher written:
```
/userdata/roms/ports/<subfolder>/<name>.sh the launcher ES lists
/userdata/roms/ports/<subfolder>/images/<name>.png box art
/userdata/roms/ports/.data/<subfolder>/<name>/ the unpacked program
```
The payload sits under `.data`, whose leading dot is what hides it from
EmulationStation — the launchers themselves are in a normal subfolder, scanned
like any other system's. Both paths carry the store's subfolder, so an uninstall
or a prune can only ever reach this store's own files.
The launcher `cd`s into the directory holding the executable before running it:
a game loads its `assets/` relative to the working directory. Python's zip
extraction drops the executable bit, so it is restored from the archive's
recorded mode.
`install` defaults to `rom`, so a config that does not mention it behaves exactly
as before.
### Per-architecture assets
Batocera runs on x86_64 mini-PCs, on the Raspberry Pi and on the ARM handhelds