Declaring Dependencies
When your tool needs to find another tool on disk at runtime — a runtime, a shared toolchain, a configuration generator — declare it as a dependency. OCX resolves the graph at install time, hardlinks every dependency's content into the consumer's environment, and composes their env surfaces according to the visibility you choose. This page covers the publisher decisions: when to declare a dependency at all, how to pin it, and how visibility changes what propagates to consumers.
When to Declare
Bundle vs. depend is the first question. Bundling means shipping the dependency's bytes inside your own archive — every install carries them. Depending means pointing at another package by digest — the consumer fetches it once and shares it across every package that references it.
Reach for a declared dependency when at least one of these is true:
- The dependency is large or commonly needed. Bundling Node.js inside every npm-tool wrapper would mean re-shipping the same ~30 MB
node-vXX.x.x-linux-x64.tar.xz(or ~57 MB Gzip equivalent) per tool. A declaredocx.sh/nodejs:24dependency means one cached install across all tools that pin the same digest. - You need a specific version of someone else's tool. Wrapping
terraformto add organisation defaults means pinning the upstreamterraformbuild — a declared dependency captures that pin in metadata, so consumers can audit it without unpacking your archive. - Your tool genuinely runs the dependency at runtime. A wrapper that shells out to
cmakeneedscmakeon disk in a known place —${deps.cmake.installPath}provides that.
Bundling stays the right call when the dependency is tiny, single-use, or version-coupled to your build (a vendored library you patched, for example).
Pinning by Digest
Every dependency entry pins by OCI digest. The tag in the identifier is advisory — it documents what you pinned against and enables future update tooling, but the digest is what OCX resolves. Two consumers installing the same package on different days, against a registry whose tags have moved, get the same dependency graph because the digest is immutable.
{
"dependencies": [
{
"identifier": "ocx.sh/cmake:3.28@sha256:a1b2c3d4e5f6...",
"visibility": "public"
}
]
}The full identifier rules — required registry component, identifier syntax, the "no version ranges" decision — live in the dependencies reference. The short version: every dependency is a fully-qualified registry/repo:tag@sha256:... string, registry mandatory, digest mandatory, tag advisory.
When You Need a name Override
By default, the placeholder ${deps.NAME.installPath} derives NAME from the last path segment of the OCI repository — ocx.sh/cmake becomes cmake, ocx.sh/myorg/cmake becomes cmake. That collides whenever two dependencies share that final segment, or when the segment is awkward to type (my-very-long-tool-name). The optional name field overrides the lookup key:
{
"dependencies": [
{ "identifier": "ocx.sh/myorg/cmake@sha256:...", "name": "myorg_cmake" },
{ "identifier": "ocx.sh/upstream/cmake@sha256:...", "name": "cmake" }
],
"env": [
{ "key": "BUILD_TOOL", "type": "constant", "value": "${deps.cmake.installPath}/bin/cmake", "visibility": "public" },
{ "key": "PATCH_SCRIPT", "type": "constant", "value": "${deps.myorg_cmake.installPath}/bin/patch.sh", "visibility": "private" }
]
}The name override must itself satisfy ^[a-z0-9][a-z0-9_-]*$ and stay at most 64 characters — same rule as entry-point names. Identifiers always carry an explicit registry (the pinning rules reject myorg/cmake@sha256:… without one).
Choosing Edge Visibility
Each dependency entry carries a visibility field, distinct from the entry visibility on env entries. This one controls how the dependency's environment propagates through the chain. Four values map onto two axes (private = the package's own runtime sees it; interface = consumers see it):
| Value | Private surface | Interface surface | Use case |
|---|---|---|---|
sealed (default) | No | No | Structural dependency — content accessed only via ${deps.NAME.installPath}, no env propagation. |
private | Yes | No | Package's own shims need the dep's env; consumers don't. |
public | Yes | Yes | Both the package and consumers need the dep's env. |
interface | No | Yes | Meta-packages that forward env to consumers without using it themselves. |
The default — sealed — is the right pick for most dependencies. A wrapper that hardcodes ${deps.cmake.installPath}/bin/cmake in an entry-point target doesn't need cmake's env to leak; it accesses cmake by path. Promote to private when your own launchers need to invoke the dep with its env applied. Promote to public when consumers should also see it (a mise-style meta-package that exposes its components directly).
The composition mechanic — what happens at diamond dependencies, how through_edge propagates visibility transitively — lives in dependencies in depth; the runtime artifact OCX writes during install (resolve.json, capturing the resolved env) is documented in environments in depth. The publisher decision compresses to one rule: pick the narrowest visibility that still covers your runtime need.
Inspired by CMake's target_link_libraries
The vocabulary maps cleanly to CMake's PUBLIC/PRIVATE/INTERFACE link visibility — what a target publishes to its build consumers. OCX applies the same intuition to the runtime env propagation graph. Use the analogy as a memory aid; the behaviours are not identical.
Ordering Matters
The order of entries in the dependencies array defines the canonical order for env composition. The first entry's environment is applied first, then each subsequent dependency layers on. path-type entries stack from later dependencies prepended onto earlier ones; constant-type entries follow the last-wins rule within the composition.
When composing a non-trivial graph, put dependencies whose env should "win" closer to the end of the array — they overwrite earlier constants and prepend to earlier path lists. Transitive deps preserve topological order (deps before dependents), and the importing package's own env always emits last, so its prepends end up highest priority on PATH. The full ordering rule lives in #dependencies-ordering.
See Also
- Dependencies reference — every field, every constraint
- Dependencies in depth — resolution, diamond dedup, garbage collection
- Env surface — entry visibility (distinct from edge visibility)
- Migration patterns — when wrapping upstream tools introduces dep declarations