Releases contract

Status: accepted, version 0.3.0 (0.3.0 adds optional category metadata; 0.2.0 adds exec_wrapper). Verified by services/releasesd/tests/releases_test.mix (offline logic) and services/releasesd/tests/releasesd_test.mix (every verb over a private broker).

releasesd follows GitHub repositories named in a per-user catalogue and installs their Linux release tarballs for that user. This page fixes the catalogue schema, the Bus verbs and the on-disk layout that other tools may rely on.

Bus identity

ItemValue
Servicereleases (mix --serve releasesd.mix --name releases)
Verbsreleases.status, .list, .check, .notes, .install, .update, .rollback, .remove, .link
BodiesJSON objects; {} or an empty body where no field is required
Successrc 0, JSON reply
Refusalsrc 10 bad request, rc 14 unknown app, rc 15 the operation failed; body {error_code, message}

Error codes: USAGE, ROOT, UNKNOWN_APP, CATALOGUE, NO_RELEASE, NO_ASSET, AMBIGUOUS_ASSET, DOWNLOAD, DIGEST, UNVERIFIABLE, LAYOUT, NO_PREVIOUS, PENDING, BUSY.

VerbBodyReply
releases.status—{catalogue, apps, installed, root, share, bin, state, authenticated, arch}
releases.list{apps?}rows from the last check
releases.check{apps?}rows after asking GitHub
releases.notes{app}{app, tag, published, url, notes}
releases.install{app, tag?}result
releases.update{apps?}results; default every installed app
releases.rollback{app}result
releases.remove{app}result
releases.link{apps?}results

A row is {app, repo, category, category_label, installed, latest, published, checked, status}, with category the catalogue's optional group and category_label its display label (null when absent), and status one of current, update, not installed, unchecked or error: <text>. A result is {app, action, version?, previous?, verified?, links?, skipped?, error?}; verified is {asset, sha256, proofs, tag}.

Catalogue, schema 1

The optional top-level categories map supplies display labels for supported groups, for example {creative: "Graphics"}. Labels must be non-empty strings of at most 64 characters without control characters. Labels are projected onto rows so Prefs can use catalogue terminology without knowing external products.

A strict-data document, by default $XDG_CONFIG_HOME/mixos/releases.mx (~/.config/mixos/releases.mx). A missing file is the default catalogue, MixOS itself: mixos-desktop and mixos-server, both from markc/mixos. It is re-read on every request.

{
  schema: 1,
  apps: [
    {name: "example", repo: "owner/example"},
    {name: "other", repo: "owner/other", asset: "(?i)linux-x86_64-full\\.tar\\.gz$"}
  ]
}
FieldRule
name^[a-z0-9][a-z0-9-]{0,63}$, unique; it becomes a directory name
repoowner/name on GitHub
assetoptional regex choosing the tarball when the default is wrong or ambiguous
categoryoptional mixos-desktop, mixos-server, creative or other, used by Prefs to group rows; omitted entries remain in Other, with legacy MixOS bundle identifiers recognised
exec_wrapperoptional list of 1–32 non-empty strings without control characters, the first an absolute path without =; see below

exec_wrapper. When link writes the app's desktop entries, every Exec= becomes Exec=<wrapper…> -- <the entry's own Exec>. The entry's own Exec keeps its field codes (%F, %U …) and is made absolute as usual. Each wrapper argument is written per the Desktop Entry Specification: % is doubled; an argument with a reserved character is double-quoted, with " ` $ \ escaped inside the quotes; then every backslash is escaped once more as a string value. The literal -- separates the wrapper from the app's command, so a wrapper can always fall back to running that command as it stands. Every Exec is wrapped, Desktop Actions included, with whitespace before the key and around = allowed. The main group and every Desktop Action must have a non-empty Exec, or the entry is refused (LAYOUT). An install, update or rollback checks this before the version becomes current, so a refusal changes nothing. If a wrapper is added while an install or rollback is interrupted and the target cannot be wrapped, the retry abandons that change: current returns to the other recorded version (if that one passes), previous is cleared, and meta.json records abandoned: {op, target, reason}. A rollback that does this reports it as its result. A wrapped entry gets DBusActivatable=false, since D-Bus activation would start the app without its Exec. TryExec stays the app's real program. The entry is rebuilt from the app's own copy on every link, so it is never wrapped twice. The link manifest records the final content. Removing the field unwraps the entry on the next link.

The default asset is the one release file matching linux[-_.]<arch>.tar.gz (x86_64, amd64 or x64; aarch64 or arm64), excluding web builds.

Verification

An install proceeds only if every checksum source the release publishes matches, and at least one exists: the asset's API digest, and a SHA256SUMS/checksums file (itself matched to its own API digest) that lists the tarball exactly once. Anything else refuses with DIGEST or UNVERIFIABLE, and nothing changes on disk.

Trust rules

  • Never as root. Every operation that changes files refuses with ROOT when run as uid 0, before taking any lock or creating any file. Installs are per user, by that user.
  • The release is checked before use. It is unpacked into a stage without owners, special permissions, ACLs, extended attributes or file flags, then refused (LAYOUT) if any symlink does not lead, step by step and through no other symlink, to an existing file or directory inside the release; if it holds a special file; or if any file is setuid or setgid. Desktop entries are read only when they are regular files inside the release.
  • Downloads are bounded. The tarball may not exceed its published size (2 GiB when none is given), a checksum file 1 MiB, and each file must arrive within 30 minutes.
  • A manifest is evidence, not permission. Each entry is {path, states}, every state a link target or a sha256. A path is replaced or removed only if it is inside this target and still in one of its states; anything else is left and reported (skipped, kept). Before changing anything, a relink publishes an intent manifest listing each path's old and new state, so an interrupted relink is recognised and finished.
  • Only managed versions are touched. A version directory is managed when it carries .verified-<version>.json. Pruning and removal touch nothing else; an app directory holding anything unmanaged is kept. current and names ending in .json are never versions.
  • A retry finishes the job. Before current moves, meta.json records the new version and previous one plus pending: {op, target}, cleared once the links are done. A later install, rollback or link first resumes a pending transition exactly: a resumed rollback is the rollback asked for; a resumed install is finished before a new rollback starts from it. Several link targets may share one opt directory (a session seeing it read-only): pending records its originating target by a random id kept in the target's own <share>/mixos/releases/.target-id (never by path: two mount namespaces can show different directories at one path). Only that target resumes and clears it, another target's link only reconciles its own links, and its install, rollback or remove refuses with PENDING until the origin has finished. Installing the version that is already current reconciles its links.
  • Serialised, in one order. Locks are always taken installation first (<root>/opt/.releases.lock, shared by every target using that opt directory, whatever its state directory), then target (<share>/mixos/releases/.lock). Install, update, rollback and remove hold both. A link holds its target and, when it can open it, the installation; through a read-only opt it cannot, and it then only reconciles its own links and never resumes a pending transition. The service also handles requests in order: while an install downloads, other requests wait, so callers allow minutes for install and update.

Layout

Root defaults to ~/.local.

PathWhat
<root>/opt/<app>/<version>/the release prefix: bin/, share/
<root>/opt/<app>/currentsymlink to the active version
<root>/opt/<app>/meta.json{name, repo, version, previous, verified, installed_at, pending?, abandoned?}
<root>/opt/<app>/.verified-<version>.jsonthe proof of each kept version
<share>/mixos/releases/<app>.links.jsonwhat was made in this target: [{path, states: [{link} or {sha256}]}]
$XDG_STATE_HOME/mixos/releases/<app>.jsonthe last check: ETag, latest release

The current and previous versions are kept; older ones are pruned. Links: bin/* into <root>/bin; desktop entries into <share>/applications with Exec/TryExec made absolute through current; hicolor icons and MIME packages as symlinks. A path that is not in the target's manifest is never replaced or removed.

Environment

MIXOS_RELEASES_ROOT, MIXOS_RELEASES_VIEW (the root as the apps see it, for installs into a container), MIXOS_RELEASES_SHARE, MIXOS_RELEASES_BIN, MIXOS_RELEASES_CATALOGUE, MIXOS_RELEASES_STATE, MIXOS_RELEASES_API (default https://api.github.com) and GITHUB_TOKEN (else gh auth token). The token is sent only to https://api.github.com.

This page as Markdown · This page describes the current main branch of MixOS.