Skip to content

Content pak reference

This page is the short authoring reference for content-paks-v1. See Adding a system or emulator for a walkthrough and the public normative contract for every rejection reason, default, merge rule, and effective-catalog detail.

pak.json becomes a content manifest when it has a top-level provides object:

{
"name": "Example",
"platform": "mlp1",
"pak_version": "1.0.0",
"min_leaf_version": "0.11.0",
"provides": {
"schema": 1,
"systems": [],
"system_extensions": [],
"cores": []
}
}

At least one of the three arrays must be non-empty. schema is exactly 1. Unknown fields inside provides and its entries are refused rather than ignored. Other top-level pak.json metadata is allowed; content_scrape is one such optional companion.

An executable launch.sh controls whether the installed pak appears in Apps:

launch.shprovidesResult
yesnoordinary app
yesyesapp plus content contribution
noyescontent only; hidden from Apps
nonoinvalid package
FieldRequiredValue
idyes^[A-Z0-9_]{2,32}$
nameyes1-64 characters
patternsyes1-32 recognized ROM-folder names
extensionsyes1-64 lowercase extensions, without dots
default_coreyescore id available after merge
rom_rootyesRoms/<folder>
image_rootyesImages/<folder>
icon_flatyespak-relative existing PNG
icon_photographicnopak-relative existing PNG or null
archive_extensionsnoarchive extensions accepted directly
archive_inner_extensionsnoROM extensions recognized inside archives
archive_modenopass_through, extract, or none
file_namesnoexact filenames that identify content
ignore_file_namesnoexact filenames discovery ignores
playlist_extensionsnoplaylist extensions such as m3u
m3u_generationnonone, auto, or manual
alternate_coresnoadditional core ids for this new system
bios_notesnoup to eight short BIOS requirements
screenscraper_platform_idsnoup to eight integers from 1-99999
groupnoCentral Scrutinizer group, or null
bios_directorynoone FAT32-safe directory component, or null

Defaults and exact length limits live in the normative contract and schema.

Every core has id, display_name, and type. Core ids match ^[a-z0-9_]{2,64}$.

For type: "retroarch":

FieldRequiredValue
libretro_nameyeslibretro core name
file_nameyespak-relative core binary
info_nameyespak-relative .info file
config_folderyesFAT32-safe Saves/States directory component
frontendnoobject that runs the core in a RetroArch the pak ships; see frontend

For type: "path":

FieldRequiredValue
pathyespak-relative existing executable

A retroarch core may not carry path, and a path core may not carry libretro_name, file_name, info_name, config_folder, or frontend; either mix is refused with core-type-field-mismatch.

Both types may declare boolean supports_menu, supports_savestate, supports_disk_control, and needs_swap; each defaults to false.

The fields requires_direct_drm, legacy_flat_core, name_map, and status are forbidden.

An optional object on a retroarch core. It makes Leaf run the pak’s own RetroArch build for that core in place of the RetroArch Leaf ships, for cores that can’t load there. Adding a system or emulator walks through it, and the LEAF-RA-1 contract says what the build has to do.

"frontend": { "path": "frontend/retroarch.sh", "protocol": "leaf-ra-1" }
FieldRequiredValue
pathyespak-relative existing executable
protocolyesexactly leaf-ra-1

The object has exactly those two fields. frontend doesn’t replace anything: libretro_name, file_name, info_name, and config_folder are still required.

Rejection reasonWhen
malformed-frontendfrontend isn’t an object, has a key besides path and protocol, or path is missing, empty, or not a string
unknown-frontend-protocolprotocol is missing or is anything but leaf-ra-1
core-type-field-mismatchfrontend is on a type: "path" core
absolute-path, path-traversal, escaping-symlink, missing-file, non-regular-file, not-executablepath breaks the path rules

provides.schema stays 1. A Leaf that predates frontend refuses the pak with unknown-field, so a pak that declares it must set min_leaf_version to the first Leaf release that supports it. Leaf copies the core into its catalog as written and fingerprints the frontend file with the pak’s other declared files, so replacing the frontend without editing provides still refreshes the catalog.

This is the only supported way to add an alternate core to an existing system:

{
"system_id": "SFC",
"add_alternate_cores": ["my_snes_core"]
}

It has exactly those two fields. It cannot override a system’s name, folders, art, extensions, or default core.

Every declared file path is relative to the pak root and must:

  • not begin with /;
  • contain no .. component;
  • remain inside the pak after resolving every symlink component;
  • exist as a regular file;
  • be executable when it is a standalone path core or a frontend.

Do not persist an SD-card mount point in a manifest. Leaf resolves the provider pak at runtime so /mnt/sdcard and /media/sdcard1 swaps remain safe.

Ownership is case-insensitively unique across system ids, patterns, ROM roots, image roots, core ids, config folders, and materialized .info filenames.

  • A collision with Leaf refuses that contribution.
  • A collision between two paks refuses both; directory order never chooses a winner.
  • Refusing a default core also refuses the system that depends on it.
  • A missing extension target is dropped and diagnosed.
  • A missing alternate core name is dropped while the rest of the extension can remain.

Diagnostics are written to .umrk/<platform>/catalog/diagnostics.json and shown on the installed pak’s Pak Rat detail page.

A package that declares provides belongs in the storefront’s content[] lane, even when it is a hybrid with launch.sh. Its pakrat.json uses "kind": "content", targets a concrete platform, and gates every published version with min_leaf_version. The same id must never appear in both apps[] and content[].

Use the ScummVM-pak repository as the build, licence, Pak Rat metadata, and CI reference.