Skip to content
ocx
install

Publish for several platforms

By the end of this page, one tag such as acme/mytool:1.0.0 resolves to the right build on every platform you publish. That covers linux, darwin and windows on amd64 and arm64, plus WebAssembly. You need one built bundle per platform and write access to a registry.

The naive approach is one tag per platform, such as acme/mytool:1.0.0-linux-amd64. That pushes platform resolution onto every consumer’s install script. OCX uses OCI Image Indexes instead: one tag, several manifests, and OCX picks the right one at install time.

An image index is a manifest of manifests, with one descriptor per platform. When a consumer runs ocx package install acme/mytool:1.0.0, OCX fetches the index and finds the manifest for the consumer’s platform. It pulls only that manifest’s layers.

You do not assemble the index yourself. Push each platform separately under the same tag. When ocx package push sees a tag that already has a manifest, it merges the new platform into the existing index rather than replacing it.

The ocx_mirror tool runs the same pattern from a YAML spec. By hand, the flow has three steps.

  1. Bundle each platform with ocx package create. Pass -i acme/mytool:1.0.0 -p <platform> and let -o . infer the output name. Each call drops a <name>-<tag>-<os>-<arch>.tar.xz archive, where <name> is the last segment of the OCI repository. It also drops a sibling <...>-metadata.json sidecar, which ocx package push picks up automatically. Bundle anatomy explains the sidecar and the inferred name.
  2. Push the first platform with --cascade. The flag keeps your rolling aliases (1.0, 1, latest) in sync. Omit -m, because push reads the sidecar next to the layer.
  3. Push every other platform with --cascade too. OCX detects the existing manifest and merges the platform into the image index. It re-points the tag at the index digest and updates each rolling alias.
Terminal window
ocx package create build -i acme/mytool:1.0.0 -p linux/amd64 -m metadata.json -o .
ocx package create build -i acme/mytool:1.0.0 -p linux/arm64 -m metadata.json -o .
ocx package push --cascade -p linux/amd64 -i acme/mytool:1.0.0 mytool-1.0.0-linux-amd64.tar.xz
ocx package push --cascade -p linux/arm64 -i acme/mytool:1.0.0 mytool-1.0.0-linux-arm64.tar.xz

After both pushes, acme/mytool:1.0.0 resolves to an image index with two platform descriptors. Any rolling alias points at the same index digest. A consumer on Linux/arm64 fetches only the arm64 manifest’s layers, and an amd64 runner fetches only the amd64 ones. Add -p darwin/arm64, -p darwin/amd64 or -p windows/amd64 the same way for the rest of the matrix.

The recording also runs ocx index update and ocx package install after the second push. It shows the consumer side: one tag, the right platform’s layers fetched, and the binary on the candidate symlink ready for ocx package exec.

If your Linux builds differ by C library, publish glibc and musl variants under the same tag. The selection rules and the libc.glibc and libc.musl tags live on that page.

The recommended pattern is one metadata.json that covers every platform. Env entries, dependencies and entrypoints apply uniformly, and the platform-specific bits live in the archive.

When platforms genuinely diverge, a different metadata file per platform is possible. Windows might need different env keys, or a platform might use different entry-point names. Hand-driven publishers pass --metadata <path> per push. The ocx_mirror spec accepts a per-platform override, where the metadata block names the default plus the overrides:

metadata:
default: metadata.json
platforms:
darwin/amd64: metadata-darwin.json
darwin/arm64: metadata-darwin.json
windows/amd64: metadata-windows.json
windows/arm64: metadata-windows.json

Each per-platform manifest’s digest depends only on its own bytes. Pushing the same archive bytes twice gives the same manifest digest, so there is no platform-side rebuild churn. Re-running ocx package create produces a different archive each time, and stable archives explains why.

The index digest is a function of its descriptors. It changes only when you add a platform or push a new build for an existing one. That stability lets --cascade work across multi-platform releases. The rolling tag points at the index, the index points at per-platform manifests, and every layer caches independently.

A .wasm file is not a native executable. It needs a runtime, and the same bytes run unchanged on every host that has one. That makes it a poor fit for any, which claims no platform requirement at all. A WASI 0.1 module and a WASI 0.2 component target different ABIs, so a consumer must say which one it wants.

OCX gives each WASI ABI its own platform, so the choice is expressed like any other platform choice:

PlatformArtifact
wasip1/wasmA module targeting the WASI 0.1 (Preview 1) ABI
wasip2/wasmA component targeting the WASI 0.2 (Preview 2) ABI

Publishing is the ordinary per-platform push. Nothing about the flow is wasm-specific:

Terminal window
ocx package create -i my/tool:1.0.0 -p wasip1/wasm -m metadata.json -o tool.tar.xz ./dist
ocx package push -p wasip1/wasm -i my/tool:1.0.0 tool.tar.xz

Consuming one always names the platform explicitly. No machine reports wasip1 or wasip2 as its host. An omitted --platform therefore resolves to the native host platform and will not find a wasm manifest:

Terminal window
ocx package install -p wasip1/wasm my/tool:1.0.0

A package built once with --platform any can still depend on a package that ships different manifests per platform. A shell script or Python entry point that wraps a native binary is the usual case. A dependency identifier carries only one digest, so which platform’s build does that digest name?

ocx package create resolves the dependency against the single --platform you declared for the bundle. It uses the directed compatibility relation that every platform decision in OCX routes through. A concrete --platform pins each dependency to the one compatible manifest digest. The any case needs two extra rules.

An any-targeted package performs no platform-specific resolution, so it can only depend on packages that themselves offer an any manifest. This follows from the compatibility relation: an any requirement is satisfied only by an any offer.

{ "dependencies": [{ "identifier": "ocx.sh/acme/mytool:1.0" }] }

ocx package create --platform any resolves this to a single manifest digest, bare on the identifier:

{
"dependencies": [
{ "identifier": "ocx.sh/acme/mytool:1.0@sha256:aaaa..." }
]
}

Suppose ocx.sh/acme/mytool:1.0 ships only platform-specific manifests (linux/amd64, darwin/arm64 and so on) and no any build. Then create fails with exit 65, naming the dependency and listing what it does offer. There is no partial coverage to derive. A platform-agnostic package either leans on a platform-agnostic dependency, or it cannot depend on that package under --platform any.

A leaf manifest carries no platform descriptor of its own. The platform lives in the image index entry one level up. A dependency pinned by a bare @digest therefore cannot be verified as any-offered from the digest alone.

ocx package create --platform any refuses that verification job. It resolves against an index, so a digest it did not resolve itself carries no evidence either way. It rejects a direct digest pin anywhere in an any-targeted bundle’s dependency list (exit 65), including one present before create ran. Pin the dependency through create, as in the any-deps rule, instead of writing a digest by hand.

ocx package push has the registry in hand, so it checks the stronger claim directly. It re-fetches the dependency’s image index by its advisory tag. It accepts the pin only if that index advertises the pinned digest as any. That means a flat manifest whose digest matches, or an index entry declaring platform any. Otherwise the push fails (exit 65), naming the dependency and the digest.

Concrete-targeted bundles are unaffected by either check. A direct digest pin is fine there, because the platform being published is known and an any claim was never in question.

A pin is a snapshot of the dependency’s platform coverage when create ran, or push for the provenance check. It is not a live query. Suppose the publisher of ocx.sh/acme/mytool:1.0 later adds an any manifest. Your published package does not pick it up. Re-run ocx package create --platform any against a refreshed index, then ocx package push, to re-resolve.