Developer SDK for Marginalia packages.
This repo defines the shared contract between package authors, the firmware, and the registry/hub:
- manifest schema
- v1 compatibility kinds and execution classes
- v2 component roles, activation, and semantic requirements
- freestanding C ABI v1 header for native components
- compatibility rules
- reference package structure
The v1 contract remains supported for the Xteink X3/X4 firmware line. Manifest v2 is the migration contract for
multi-component packages; current firmware can consume its declarative shape and admits the first native Service host
for the marginalia-c-1 ABI. App and Provider roles remain gated on their role-specific hosts. Native package
activation remains gated on measured hardware admission and recovery validation, so a structurally valid package may
still be installable while its executable components are unavailable.
Installation and activation are separate lifecycle events. Firmware may report activationPending after it has
validated and staged an archive; package files are not hot-swapped into running hosts, and the candidate is not a
confirmed running version until a later boot completes its recovery trial. This is device-local state, not a new
manifest field or a registry status.
Marginalia packages are either:
static: validated manifest data with no executable package runtimemodule: loaded into the firmware host for themes, sleep screens, reader hooks, widgets, and integrationsapp: a standalone experience with its own navigation lifecycle
Package kinds:
themesleep_screenreader_moduleintegrationapp
Packages can also declare static contributes entries for firmware-visible extension points. The first contribution
point is catalog.providers, which lets integration packages add OPDS/RWPM-style catalog providers without adding a
new firmware feature for each provider. Static contribution packages should use execution: "static" and do not need
runtime entrypoints.
Manifest v2 replaces package-wide behavior with components of type app, service, provider, or contribution.
Use --schema-version 2 with the scaffolder to create a v2 package. The SDK normalizes v1 and v2 to the same component
view for validation. Native executable components currently use the fixed ABI-v1 export
marginalia_module_entry_v1; role callbacks are selected by the descriptor returned from that export.
App components use the ABI-v1 complete packed 1-bpp framebuffer target and semantic input event definitions in
native_abi_v1.h. The firmware owns the foreground loop and display timing;
the SDK header does not grant direct renderer, GPIO, raw SD, or RTOS access. ABI minor 2 adds a bounded asynchronous
package asset/data storage contract; read completions are delivered through typed events and larger reads are chunked by
the package. The same descriptor carries bounded resource declarations; the device profile, not the catalog, decides
whether an installed native component can activate.
schema/manifest.v1.schema.jsonschema/manifest.v2.schema.jsonschema/theme.v1.schema.jsoninclude/marginalia/native_abi_v1.hdocs/package-contract.mddocs/community-sdk.md
python3 tools/validate_manifest.py path/to/manifest.jsonThe default local profile matches what current firmware accepts for SD/Wi-Fi side loading. Use the stricter publish
profile for packages that are ready for a registry or hub:
python3 tools/validate_manifest.py --profile publish path/to/manifest.jsonThe validator selects the schema from schemaVersion. Compatibility failures are reported separately from schema
errors; a structurally valid v2 native package may be installable while one executable role remains unavailable until its
role-specific native ABI host exists.
python3 tools/create_package.py org.example.hangman "Hangman" --kind app --output ./packagesFor the v2 component shape:
python3 tools/create_package.py org.example.dark-mode "Dark Mode" --schema-version 2 --kind theme --output ./packagesThe command writes a side-loadable package folder with:
manifest.jsonREADME.mdsrc/entrypoints.json
Use --profile local for the smallest manifest accepted by current firmware. Use the default publish profile when the
package is meant to become a registry or hub entry.
python3 tools/build_package.py ./packages/hangman --output ./dist --jsonThe builder validates the manifest, binds a v2 native component's inner SHA-256 to the exact file it archives, runs the dependency-free native ELF preflight, rejects paths the firmware side-loader would reject, creates a deterministic package archive name, and prints the archive SHA-256 checksum that registry and hub entries should publish. Firmware re-runs the authoritative verifier after download.
Legacy schema v1 packages remain metadata/compatibility packages and do not need a native ELF artifact; the native artifact binding contract starts with schema v2.
To inspect an artifact without building an archive:
python3 tools/inspect_native_module.py path/to/module.native --jsonFirmware-safe archives are capped at 96 files, 512 KiB total uncompressed data, 128 KiB per file, and 180 bytes per relative archive path.
For manifest v2, components[].requires describes semantic host capabilities. The firmware evaluates those requirements
against its active hardware profile and role host at scan time, so a missing component capability can leave the package
installed and visible while that component remains unavailable. SDK/registry compatibility is intentionally package-level
and does not make a missing component capability a safety verdict or package-deletion decision.
The optional root dependencies list describes package activation prerequisites. Each entry is { "id", "version" },
where version is a minimum MAJOR.MINOR.PATCH version. It does not download or enable anything: the device user must
install and enable every dependency. A missing, disabled, incompatible, quarantined, cyclic, or too-old dependency leaves
the declaring package installed but unavailable until the user changes the package set. Dependencies are resolved from one
bounded device snapshot and are started before dependents; teardown is attempted in reverse order.
The X3/X4 App role currently has a hardware-only fixture in
../marginalia-examples/hardware-fixtures/native-smoke-app. Its local archive
and ELF preflight are useful for the loader matrix, but the normal target
profile still reports App ABI support as unavailable until physical X3/X4
results exist. The typed Provider role has the corresponding deterministic
fixture in
../marginalia-examples/hardware-fixtures/native-reader-sync-provider; it
exercises reader.sync/1 records without opening a transport or reading
credentials. Its preflight is likewise not a runtime or safety verdict, and
the normal target profile keeps Provider ABI support unavailable until the
network/credential host and physical recovery matrix are complete.
python3 tools/build_catalog_entry.py ./packages/hangman \
--archive-output ./dist \
--entry-output ./entries/org.example.hangman.json \
--artifact-url https://example.org/org.example.hangman-0.1.0.mpkg.zip \
--source-url https://github.com/example/marginalia-packages.git \
--source-ref v0.1.0 \
--source-path hangmanThe command builds the .mpkg.zip, computes its checksum and size, and writes a catalog entry shaped for
marginalia-registry. The source fields make registry entries reviewable in the same spirit as RT-Thread package
metadata: the artifact is what firmware installs, while the source repository and ref are what humans inspect.