Skip to content
ocx
install

How patches work

A patch changes the environment a package runs in, without changing the package. An operator publishes a small descriptor and one or more companion packages to a registry they control. OCX composes the companions into the environment of every matching package at execution time. The base package on disk stays exactly as its publisher shipped it.

This page explains how a descriptor matches packages, how a companion joins its target, and where its pins live. To apply patches, read Patch packages for your infrastructure. To author them, read Maintain patch companions.

A patch descriptor is a small JSON document stored in your organization’s OCI registry. It declares rules: when an installed package’s identifier matches a glob pattern, apply these companion packages to its execution environment.

At ocx exec time, OCX fetches the descriptor and identifies the matching companions. It then composes their environment entries as part of the package they match. Each entry lands on the surface the package itself is read through, as if the package had declared it. The base package is never modified. See Companions are part of their target.

A companion’s integrations block, if it declares one, contributes the same way any package’s does. The rows are attributed to the companion’s own identifier. They are never merged into anything the base or another companion declared. The environment composition reference gives the row shape and ordering rules in Patch companions contribute too.

{
"version": 1,
"rules": [
{
"match": "*",
"packages": ["registry.corp.example/infra/ca-bundle:latest"]
},
{
"match": "ocx.sh/java:*",
"packages": ["registry.corp.example/infra/jdk-truststore:1.0"],
"required": true
}
]
}

The match field is a flat glob with three shapes:

  • registry/repo (no tag, no digest) matches every tag and digest of that repository.
  • registry/repo:<tag-glob> matches by tag. A reference with no tag and no digest counts as latest. A digest-only reference has no tag.
  • registry/repo@<digest-glob> matches by digest, whatever advisory tag the reference carries. registry/repo@* matches any digest-pinned reference.

* matches any character including /, :, and @, so * alone matches every package. ocx.sh/java:* matches any version of the JDK hosted at ocx.sh, including a reference with no tag. A glob such as ocx.sh/java* also matches ocx.sh/javafx.

Ruleocx.sh/java:21ocx.sh/javaocx.sh/java@sha256:Hocx.sh/java:21@sha256:H
ocx.sh/javamatchmatchmatchmatch
ocx.sh/java:*matchmatchnomatch
ocx.sh/java:latestnomatchnono
ocx.sh/java:21*matchnonomatch
ocx.sh/java@*nonomatchmatch
ocx.sh/java@sha256:Hnonomatchmatch

The columns are the forms a reference takes:

  • ocx.sh/java:21: ocx package install ocx.sh/java:21.
  • ocx.sh/java: ocx package install ocx.sh/java, with no tag.
  • ocx.sh/java@sha256:H: a digest-only ocx.toml entry, with its resolved digest.
  • ocx.sh/java:21@sha256:H: a tagged ocx.toml entry, with its resolved digest.

H is a concrete digest. A digest in a pattern matches the platform-specific manifest digest, not the multi-platform index digest.

A bare ocx.toml entry such as java = "ocx.sh/java" counts as :latest, so it takes the fourth form: ocx.sh/java:latest@sha256:H.

ocx package install ocx.sh/java:21 discovers on the reference as you typed it: the first two forms. ocx package install ocx.sh/java@sha256:H matches the third. The lock-driven commands (ocx lock, ocx add, ocx update, ocx pull) and composing the environment at exec or env match the pinned reference with its digest: the last two forms. That is why an ocx.sh/java@* rule matches at exec but not when you install ocx.sh/java:21 by tag.

A registry-wide glob such as ocx.sh/*:* also matches digest references, because the : in sha256: satisfies it. *:* can match a registry port as well (localhost:5000/java), so scope a rule to a registry host unless matching everything is the intent.

When one package is declared under two tags, the tag of the first binding in selection order is the one matched. Selection order is the groups in the order you select them (all puts default first), then bindings sorted by name. ocx direnv export and the global toolchain ignore the selection order and take the first binding in lock order (groups, then bindings, by name).

Tags are advisory here. A companion that must always apply should match on the bare repository, or a digest, rather than a tag. When the lock is stale, ocx direnv export and the global toolchain match patch rules without the declared tag until ocx lock runs.

Rules are evaluated in order and unioned. A Java install matched by both rules above gets both companions composed in.

A companion’s env var carries the same visibility any package’s var does. It is read on the surfaces that visibility names for the package it patches. A package has two surfaces. The interface surface is what its consumers see. The private surface is what its own generated launchers see, also shown by ocx package env --self and ocx package exec --self.

Companion varTarget’s consumers, ocx package env, shells, dependentsTarget’s own launchers, --self
privatenoyes, when the target is a root of the composition
interfaceyesno
publicyesyes, when the target is a root of the composition

A var that declares no visibility is private.

The private column applies only when the target is a root of the composition. A target that is a dependency of the package being composed is admitted through its interface alone. Its companion then contributes its interface and public vars wherever that dependency’s interface surface reaches, and its private vars never load. A private var therefore cannot leak into the consumers of a package that depends on the target.

Three more rules follow from “as if the target had declared it”:

  • A companion’s own dependencies are admitted the same way. Its private dependencies load only where its private side loads.
  • A catch-all rule that matches a root and its dependencies composes the union of the two sides under --self and in launchers. Each entry applies once. The consumer view takes the interface side only.
  • ${installPath}, ${deps.*} and ${self.env.*} inside a companion resolve against the companion, not the target.

For example, plantuml has a private dependency on a JRE, and its entrypoint runs java -jar ${installPath}/plantuml.jar. A companion matched to plantuml declares JDK_JAVA_OPTIONS as private:

{
"env": [
{
"key": "JDK_JAVA_OPTIONS",
"type": "list",
"separator": " ",
"value": "-Dhttps.proxyHost=proxy.corp.example",
"visibility": "private"
}
]
}

Only plantuml’s launcher sees the flag. The JRE is shared by other Java packages, and neither it nor plantuml’s consumers receive it.

Patch entries apply after all package env, so site policy wins over what a package declares. Project [env] and --env apply after them.

A companion brings its own dependencies into the environment, so it can name a repository the environment already carries. Suppose its closure reaches that repository at a different digest than the base, or than an earlier companion. The later companion is refused.

A required companion fails the command with exit 65. An optional one is skipped with a warning.

Two companion roots of the same repository at different tags are not a conflict. Both apply, each as its own companion. A dependency the base or an earlier companion already composed is emitted once.

A companion’s own entrypoint launchers are never put on PATH, since they run the companion’s environment rather than its target’s. Its own binaries claim is admitted, and so are the claims and launchers of its dependencies, as for any dependency.

A generated launcher finds its package by directory, and one directory serves every name that resolves to its digest. The command that composed the environment passes the names it used in OCX_LAUNCH_IDENTITIES. The launcher matches rules against each of them.

So ocx.sh/java:* reaches the JDK’s launchers when the JDK runs through ocx exec, ocx package exec, ocx package test or a lazy tool’s shim. It also reaches them from a PATH that ocx env --shell, ocx direnv export or the shell hook composed, since those exports carry the names too.

A launcher run by its absolute path, or from a PATH no ocx command composed, matches only * rules. A digest-only composition passes a name without a tag. Tag-anchored rules skip it, as they do everywhere else.

Patches are composed when OCX builds an environment to run something: ocx exec, ocx package exec, ocx package test, ocx env, ocx package env and generated launchers. Metadata-only views do not model them. ocx package inspect --closure, ocx inspect --closure and the name sets a toolchain render derives for PATH describe the closure the packages declare, without companions.

The descriptor above ships two companions for what is conceptually one CA bundle. A generic ca-bundle companion matches every package (*). A separate jdk-truststore companion matches only ocx.sh/java:*. Every CA-bundle descriptor needs this split, because each language runtime discovers trusted CAs its own way.

SSL_CERT_FILE is the closest thing to a universal override. It is an OpenSSL convention, not a language standard. curl, git and Python’s ssl module honor it because they link against OpenSSL or defer to its lookup.

Go’s crypto/x509 honors SSL_CERT_FILE and SSL_CERT_DIR only on Linux and the BSDs. On Windows and macOS it calls the OS-native certificate store and ignores both variables. Java never reads SSL_CERT_FILE. It has no environment variable for its default trust store at all.

RuntimeCA override
OpenSSL-linked tools (curl, git, Python ssl)SSL_CERT_FILE / SSL_CERT_DIR
Go crypto/x509SSL_CERT_FILE / SSL_CERT_DIR on Linux/BSD only; ignored on Windows and macOS
JavaNo environment variable. Set the javax.net.ssl.trustStore system property via JAVA_TOOL_OPTIONS, or keytool -importcert the certificate into the JVM’s cacerts keystore directly.

A descriptor rule’s match glob is how a patches tier says “this runtime needs its own companion”. Write one rule per runtime whose CA mechanism differs. Each rule points at the package that knows how to install it. No single companion reaches every runtime at once.

JAVA_TOOL_OPTIONS is a space-separated option list, not a single value. A constant entry would erase the flags the base JDK package or another companion already put there. A path entry would join with the platform path separator instead of a space, and it would prepend instead of append. The jdk-truststore companion above declares the variable as list, so its trust-store flag appends after anything already contributed:

{
"env": [
{
"key": "JAVA_TOOL_OPTIONS",
"type": "list",
"separator": " ",
"value": "-Djavax.net.ssl.trustStore=${installPath}/cacerts",
"visibility": "public"
}
]
}

visibility is public: the flag reaches the JDK’s consumers, and the JDK’s own launchers when the JDK is the package being run. A JDK pulled in as a dependency receives the flag through its interface alone. See Appending option lists for when to reach for list in your own packages.

A catch-all rule such as "match": "*" matches every root and every dependency. Under --self and in launchers it composes the union of the private and interface sides. The consumer view takes the interface side only.

A value meant for everyone, like a CA bundle path or a truststore flag, belongs on a public var. A private var misses consumers, and an interface var reaches a launcher only through a dependency the rule matches.

OCX writes a companion’s tag-to-digest binding to $OCX_HOME/state/patch-companions/, one JSON file per patch-registry repository. The local index ($OCX_HOME/index/) never receives it. A companion repository owns zero bytes there, with no root document and no dispatch object. So committing an index tree to git never picks one up.

A companion is a package the descriptor named on the operator’s behalf, not one you asked for by name. Its binding therefore belongs to the patch tier’s own state, not to the package-tier pin that ocx index update maintains. Two consequences follow:

  • ocx index update can never make a companion resolvable.
  • --frozen freezes that index and does not reach a companion at all.

Patches float by design.

A companion’s pin advances only on ocx patch sync, including for a rolling tag the descriptor keeps naming. Composition reads the pin. It records one only when a required companion has none, and it never advances an existing pin. Under a patch snapshot no command records a pin, including install, pull and lock.

To pick up a changed descriptor or a new companion, run ocx patch sync while online. Then freeze again, as described in Freeze for reproducible builds.