Skip to content

Platforms

Every OCX package targets a platform — a specific OS/architecture pair, an OS/architecture pair refined by CPU variant, a set of required OS capabilities, or any for platform-agnostic content. OCX needs one string form for that value that works everywhere: the --platform flag, ocx.lock's per-platform digest map, and the build receipt ocx package create writes beside a bundle. This page is the canonical reference for that string and for the compatibility relation OCX evaluates it against.

Grammar

A platform is either the literal any, or an os/arch pair optionally refined by a CPU variant and a list of required OS features:

os/arch[/variant][+feature[,feature...]]      |      any
text
platform      = "any" | specific ;
specific      = os "/" arch [ "/" variant ] [ "+" feature-list ] ;

os            = "linux" | "darwin" | "windows" | "wasip1" | "wasip2" ;
arch          = "amd64" | "arm64" | "wasm" ;

variant       = value ;                       (* e.g. "v7", "v8" *)
feature-list  = feature { "," feature } ;
feature       = value ;                       (* e.g. "libc.glibc" *)
ExampleMeaning
linux/amd64Linux on x86-64, no refinements
linux/arm/v7Linux on 32-bit ARM, variant v7
linux/amd64+libc.glibcLinux on x86-64, requires the libc.glibc os.features tag
linux/amd64+libc.glibc,libc.muslLinux on x86-64, requires both glibc and musl (a dual-libc override)
linux/arm64/v8+libc.glibcEvery field combined
wasip1/wasmA WebAssembly module targeting the WASI 0.1 ABI
anyPlatform-agnostic — no OS, architecture, or feature requirement

/ introduces variant, and a single + introduces the comma-separated feature list — each occupies a structurally distinct slot, so linux/arm64/v8 (a variant) can never be confused with a +-list. os and arch are closed sets; adding a value to either requires an OCX release. any carries no fields — any+libc.glibc and any/amd64 are both rejected as malformed.

Supported pairs

Being in the os set and being in the arch set is not enough — the pairing has to name an artifact somebody can build. linux/wasm and wasip1/amd64 are each assembled from valid components and describe nothing, so OCX enforces an explicit list rather than the cross product:

PlatformTier
linux/amd64Native
linux/arm64Native
darwin/amd64Native
darwin/arm64Native
windows/amd64Native
windows/arm64Native
wasip1/wasmWasm
wasip2/wasmWasm

The two tiers differ in how a platform is reached, not in how it is written. Native pairs are the ones OCX itself runs on and CI tests against, so host detection can produce any of them and they are what an omitted --platform resolves to. Wasm pairs are distribution labels only: no host reports wasip1 or wasip2, so they are reachable exclusively through an explicit --platform — see publishing for wasm.

A pairing outside this list is refused while parsing, before resolution is attempted, and the refusal lists the pairs above.

This is the single grammar for every platform string surface in OCX: --platform, ocx.lock's [tool.platforms] keys, and the build receipt ocx package create writes beside a bundle. One publisher libc tag needs one encoding everywhere it appears, not a different spelling per file.

Escaping

variant and each feature value are registry-controlled strings — a publisher could in principle choose a value containing /, +, or ,. To keep the grammar unambiguous, OCX percent-escapes the four structural characters wherever they appear inside a value:

CharacterRoleEscape
%escape introducer%25
/os/arch/variant separator%2F
+feature-list introducer%2B
,feature separator%2C

Every other byte passes through verbatim, so ordinary values — v7, v8, libc.glibc — are byte-identical to their raw form and never need escaping in practice. A % not followed by one of the four two-character codes above is a parse error.

This makes the platform string an injective, round-tripping encoding of the underlying value: parsing a platform string and re-rendering it always produces the identical string back. That property is what lets the same string double as both the CLI flag value and a BTreeMap key in ocx.lock — no separate key-encoding format is needed.

Compatibility

Selecting the right manifest for a host is not a string comparison. A host that advertises linux/amd64+libc.glibc should install a manifest published bare as linux/amd64 (no libc requirement declared), but a manifest published linux/amd64+libc.musl should not match that same host at all — the strings differ, and the relationship between them determines the outcome, not equality. OCX evaluates a directed relation, is_compatible(required, offered), everywhere a platform decision is made: resolving a fresh install against an OCI Image Index, reading a locked digest out of ocx.lock, and pinning a dependency during ocx package create.

The relation

is_compatible(required, offered) reads "does offered satisfy the requirement required?". required is what the caller needs satisfied — the detected host, an explicit --platform value, or a lock lookup key. offered is a candidate — an image-index child platform.

The relation is not symmetric, and the asymmetry is deliberate:

  • An any offer satisfies every requirement. A platform-agnostic artifact (a shell script, a JAR) runs anywhere, so it matches any host.
  • An any requirement is satisfied only by an any offer. A host OCX could not detect (an unsupported OS, a failed probe) can run platform-agnostic content, never a Specific binary — there is nothing to check compatibility against.
  • os.features inverts. A feature value on the offer names a capability the binary demands of the host, so the direction flips relative to the required/offered naming: every feature the offer declares must be present in what the requirement offers as host capabilities (offered.os_features ⊆ required.os_features). variant does not invert — it is strict equality, gated on the offer declaring a value at all. An offer that leaves variant unset imposes no constraint.
RequiredOfferedCompatible?Why
linux/amd64linux/amd64YesExact match, no refinements
linux/amd64anyYesAn any offer satisfies every requirement
anylinux/amd64NoAn any requirement is satisfied only by an any offer
linux/arm64/v8linux/arm64YesOffer leaves variant unset — unconstrained
linux/arm64linux/arm64/v8NoOffer declares variant, required has none
linux/amd64+libc.glibclinux/amd64Yes{} ⊆ {glibc} — a bare offer runs on a glibc host
linux/amd64+libc.glibclinux/amd64+libc.muslNo{musl} ⊄ {glibc}
linux/amd64+libc.glibc,libc.musllinux/amd64+libc.glibcYes{glibc} ⊆ {glibc,musl} — a dual-libc host runs a single-libc offer

The libc differentiation page walks through the dual-libc case end to end, including how OCX detects which libc families a host provides.

Because the inversion makes an empty os.features a positive claim — "this artifact demands nothing of the host, so every host matches" — omitting a feature is never a neutral act. ocx package create enforces that for the libc namespace on Linux targets: it reads the packaged binaries' dynamic loader and refuses a platform whose os.features do not cover what they need.

Scoring

An image index can offer more than one compatible manifest for the same requirement — a bare linux/amd64 entry alongside a linux/amd64+libc.glibc entry, for instance. OCX picks the most specific compatible offer, ranked lexicographically:

  1. Specific outranks any. A linux/amd64 offer beats a co-present any offer for a linux/amd64 requirement — a targeted binary is always preferred over a platform-agnostic fallback when both are available.
  2. Among Specific offers, more matched features win. A linux/amd64+libc.glibc offer beats a bare linux/amd64 offer for a glibc host — the more specific declaration wins.
  3. A tie is ambiguous, never guessed. Two offers scoring equally — for example a dual-libc host against separate libc.glibc and libc.musl entries, each declaring exactly one feature — cannot be ranked against each other. OCX reports the ambiguity (exit 65) instead of picking one; the caller disambiguates with an explicit --platform.

This single relation and scoring rule is what makes fresh installs, ocx.lock reads, and dependency pinning agree: the same host or requirement always resolves to the same answer regardless of which of the three call sites is asking.

Shared Digests

Two distinct platform keys pointing at the same manifest digest is legitimate and expected — OCX never rejects it. The canonical case is Rosetta 2: a publisher who ships only a darwin/amd64 build can also list that identical manifest under darwin/arm64, so Apple Silicon hosts install and run it through Rosetta's x86-64 translation instead of failing to find an arm64 build at all. The two platform keys are genuinely different — one names the binary's native architecture, the other names a host that can run it via translation — but both may point at the same bytes.

Why this isn't a bug

It is tempting to think two keys with the same digest indicates a publishing mistake — surely a platform-specific build should have a unique digest? OCI Image Index entries are content-addressed by design: the digest identifies the bytes, and nothing in the spec (or in OCX) requires a one-to-one mapping between platform descriptors and content. A generic OCI client that deduplicates identical layers across platform entries relies on exactly this property.

See Also

  • Multi-Platform Packages — the publisher workflow: building, pushing, and libc differentiation
  • --platform flag reference — the CLI surface for every command that resolves against a platform
  • Dependencies — how ocx package create resolves and pins a dependency identifier's manifest digest against a declared platform
  • Lock format — the ocx.lock [tool.platforms] table