- docs
- Publishing
- Declare the environment
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.
Pick the variable type
Section titled “Pick the variable type”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.
Append to option lists
Section titled “Append to option lists”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:
| Variable | Consumer behavior |
|---|---|
JDK_JAVA_OPTIONS, NODE_OPTIONS (scalar flags), GODEBUG | Last occurrence wins, so append order gives the override you expect. |
RUST_LOG | Most-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 line | First 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.
Reference install paths with tokens
Section titled “Reference install paths with tokens”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 owncontent/directory.${self.env.KEY}is the resolved value of this package’s ownKEYvariable, declared earlier in the sameenvarray. It takes no modifier, because OCX cannot know whether the variable holds a path.${deps.NAME.installPath}is a declared dependency’scontent/directory.NAMEis the last path segment of the dependency’s repository or its explicitnamefield.
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.
Choose visibility
Section titled “Choose visibility”Each env entry carries a visibility field that selects which surface it contributes to:
- Interface surface: what consumers see when they run
ocx package exec mypkg -- <cmd>or composemypkgas a dependency. Public PATH entries andJAVA_HOMElive here. - Private surface: what the package’s own generated launchers see at exec time. Internal flags and lock-file paths live here.
| Value | Interface surface | Private surface | Use case |
|---|---|---|---|
private (default) | No | Yes | Internal paths the package’s own launchers need. |
public | Yes | Yes | Variables consumers should see, such as PATH, JAVA_HOME and prefix paths. |
interface | Yes | No | Values 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.
Decision guide
Section titled “Decision guide”| Variable pattern | Recommended 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) |
Avoid last-wins conflicts
Section titled “Avoid last-wins conflicts”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.
Check what consumers receive
Section titled “Check what consumers receive”Run ocx package env on the pushed package to see the consumer view. Add --self to see the private view:
ocx package env ghcr.io/acme/mytool:1.0.0ocx package env --self ghcr.io/acme/mytool:1.0.0The 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.
See also
Section titled “See also”envreference for every field on anenventry- Environments in depth for composition order, the edge filter and conflicting constants
- Add named launchers for when entrypoints replace exposed
PATHentries - Dependency-edge visibility for how dependency declarations propagate env