Skip to content
ocx
install

Declare the environment

By the end of this page the env block of your metadata.json declares every variable your package contributes, each with the right type and visibility. The block is the contract between you and every consumer. A good surface composes cleanly with dependent packages, and a bad one leaks internal state into consumer shells. You need a bundle and a metadata.json to edit. The metadata reference documents every field, and environments in depth owns the composition rules.

Most entries are one of two shapes. A path variable prepends a directory onto a PATH-like list. A constant variable replaces the value outright. Path entries from several packages stack, so every package’s bin/ ends up on the consumer’s PATH. Constants do not stack, and when two packages declare JAVA_HOME, the last one in dependency order wins.

Pick path for directory lists such as PATH, MANPATH, LD_LIBRARY_PATH and PKG_CONFIG_PATH. Pick constant for prefix variables such as JAVA_HOME, CARGO_HOME and CMAKE_ROOT, and for version markers. Neither shape fits an option-list variable, so use list for those.

Some variables are a flat, ordered list of flags that a runtime reads at once. Examples are JDK_JAVA_OPTIONS, JAVA_TOOL_OPTIONS, NODE_OPTIONS and Go’s GODEBUG. A path entry joins with the platform path separator and prepends, which is wrong for a space-joined or comma-joined string. A constant entry replaces the whole value, so the flags your dependency declared for itself vanish.

The list type appends your contribution, joined by a separator you choose. It first removes any earlier occurrence of the same contribution. Two packages that contribute -Xmx1g and -ea to JDK_JAVA_OPTIONS both survive. If one package re-declares a flag, the value does not grow.

{
"key": "JDK_JAVA_OPTIONS",
"type": "list",
"separator": " ",
"value": "-Xmx2g",
"visibility": "interface"
}

The separator is required here, unlike in ocx.toml or --env. Package metadata is the wire, and no human is present to be told what was assumed. It must be non-empty and must not contain =, a newline or a carriage return. Every downstream export is line-oriented, and a separator that ends a line stops being a delimiter. See Separator Is Required for the full rule, including per-key agreement across a composition.

Check how the consumer resolves duplicates

Section titled “Check how the consumer resolves duplicates”

The list type appends. Whether the last contribution wins depends on how the consuming tool resolves duplicates, and OCX never parses list elements to enforce it. This survey covers common option-list variables:

VariableConsumer behavior
JDK_JAVA_OPTIONS, NODE_OPTIONS (scalar flags), GODEBUGLast occurrence wins, so append order gives the override you expect.
RUST_LOGMost-specific target wins, not append order. A broader directive appended after a narrower one (warn after my_crate::noisy=trace) does not silence it. Treat list on RUST_LOG as layering directives, not overriding them.
-I/-L search-path flags on a compiler command lineFirst occurrence wins. A later -I adds a fallback search directory and never an override. The polarity is the reverse of the other rows.

JDK_JAVA_OPTIONS has no such problem, because the JVM launcher defines a quote grammar. Single or double quotes wrap an argument that contains whitespace, and the launcher strips the pair. Quote a contribution the way you would on a command line. OCX’s dedup never parses list elements, so a quoted value passes through intact.

A value cannot hardcode an install path, because content-addressed storage puts the same package at a different path on every machine. The ${...} tokens name “wherever OCX put me” or “wherever my dependency landed”. The install-path tokens take an optional :native or :posix render modifier that controls path-separator rendering only.

  • ${installPath} (and its exact alias ${self.installPath}) is this package’s own content/ directory.
  • ${self.env.KEY} is the resolved value of this package’s own KEY variable, declared earlier in the same env array. It takes no modifier, because OCX cannot know whether the variable holds a path.
  • ${deps.NAME.installPath} is a declared dependency’s content/ directory. NAME is the last path segment of the dependency’s repository or its explicit name field.

A wrapper that points at its dependency’s binary uses ${deps.cmake.installPath} to keep the path stable across registry layouts:

{
"env": [
{ "key": "MYTOOL_HELPER", "type": "constant", "value": "${deps.cmake.installPath}/bin/cmake-gen", "visibility": "public" }
]
}

The ${...} namespace is closed. OCX refuses any occurrence that is not one of the three tokens above. To ship another tool’s own ${...} syntax verbatim, such as ${workspaceFolder} from devcontainer.json, write $${. Then $${workspaceFolder} publishes as the literal text ${workspaceFolder}. That is the only escape.

OCX validates every reference during ocx package create --metadata <file>, with no network, and again during ocx package push. A typo never reaches the registry. See Interpolation Tokens for the grammar and the exact refusal.

Each env entry carries a visibility field that selects which surface it contributes to:

ValueInterface surfacePrivate surfaceUse case
private (default)NoYesInternal paths the package’s own launchers need.
publicYesYesVariables consumers should see, such as PATH, JAVA_HOME and prefix paths.
interfaceYesNoValues forwarded to consumers but unused by the package, such as PKG_CONFIG_PATH and include hints.

OCX rejects sealed on env entries at parse time, because an entry that contributes to neither surface is dead configuration. The way entry visibility combines with dependency-edge visibility is in environments in depth.

Variable patternRecommended visibility
PATH pointing at ${installPath}/bin"public" (consumers need it on their PATH)
JAVA_HOME, CMAKE_ROOT, prefix vars"public" (consumers reference these by name)
MANPATH, PKG_CONFIG_PATH, ACLOCAL_PATH"public" if consumers need the content, "interface" if the package does not use these paths
Internal flags (_MY_TOOL_INIT, lock files, IPC paths)"private" (default, no annotation needed)

Say two Java distributions in one composition both set JAVA_HOME. The last one in topological dependency order wins in ocx package exec and ocx env. The first is replaced silently. The --ci flag of ocx env and ocx package env also runs a ConstantTracker pass that warns when unrelated entries collide. The full rule is in environments in depth.

Treat a conflicting constant as a signal. If a sibling package already declares your JAVA_HOME, two publishers are fighting over one slot. The consumer should depend on one of them and seal the other’s env.

Run ocx package env on the pushed package to see the consumer view. Add --self to see the private view:

Terminal window
ocx package env ghcr.io/acme/mytool:1.0.0
ocx package env --self ghcr.io/acme/mytool:1.0.0

The first command prints a table with Key, Type and Value columns that lists the public and interface entries. A package that marks PATH and JAVA_HOME as "public" shows both keys there. The second lists the public and private entries the package’s own launchers see.