- docs
- Projects
- Run binaries from your project
Run binaries from your project
Run a binary with exactly the tools ocx.lock pins, or export that environment to a shell or a CI job. Start with an ocx.toml and a current lock, and call each tool by its binding name instead of its OCI identifier.
Run a command
Section titled “Run a command”ocx exec runs a command in the default group ([tools]) environment:
ocx initocx add "kitware/cmake:4.2.0"ocx exec -- cmake --versionThe argument shape is:
ocx exec [-g GROUP[,GROUP,...]]... [NAME...] -- ARGV...-- is mandatory, and at least one token must follow it. Every token after -- is forwarded unchanged to the child. ocx exec cmake without -- exits 64. ocx exec -- echo hi with no NAME composes every binding in the default scope. Pass -g to scope to a named group:
ocx initocx add -g ci "astral-sh/uv:0.10.0"ocx exec -g ci -- uv --versionTo compose the environment from every group at once, use the all keyword:
ocx initocx add "kitware/cmake:4.2.0"ocx add -g ci "astral-sh/uv:0.10.0"ocx exec -g all -- cmake --version-g all expands to [tools] plus every declared [group.*] before env composition. Groups listed later win PATH lookup. See Project Toolchain In Depth → Running binaries for the composition order.
The -g forms select these scopes:
| Invocation | Scope |
|---|---|
ocx exec -- CMD | Default group ([tools]) only |
ocx exec -g ci -- CMD | [group.ci] only |
ocx exec -g ci,release -- CMD | [group.ci] and [group.release] |
ocx exec -g all -- CMD | [tools] plus every declared [group.*] |
ocx exec cmake -- CMD | Default scope, filtered to the cmake binding |
Omitting -g means the default group, not everything. The word all is reserved. [group.all] in ocx.toml is rejected at parse time (exit 78), and ocx add --group all is rejected with exit 64.
When you only need a specific binding from the composed set, name it:
ocx initocx add "kitware/cmake:4.2.0"# `ocx add` names the binding after the repository basename, so a two-segment# identifier still binds as `cmake` — the binding name is what `ocx exec` takes.ocx exec cmake -- cmake --versionThe name must resolve unambiguously in the selected scope. ocx exec exits 64 if a name is unknown or matches entries in more than one selected group with conflicting identifiers.
Read the exit code
Section titled “Read the exit code”ocx exec forwards the child’s exit code byte-for-byte. It sets its own code only when it fails before or while spawning the child:
| Code | Condition |
|---|---|
| 0 | The child exited 0. |
| 1 | The spawn failed after the binary resolved. |
| 64 | -- is missing, argv is empty, a -g segment is empty or unknown, no ocx.toml was found, or a name is unknown or ambiguous. |
| 65 | ocx.lock is stale (run ocx lock), or the command did not resolve in the composed environment. |
| 69 | The registry was unreachable during auto-install. |
| 78 | ocx.lock is missing (run ocx lock), or ocx.toml has a parse error. |
| 79 | A package was not found in the registry during auto-install. |
| 80 | Authentication failed during auto-install. |
The exit codes reference lists every code the CLI uses. Clap usage errors exit 64 instead of clap’s default 2.
Choose exec or package exec
Section titled “Choose exec or package exec”Short rule: if you have an ocx.toml, use ocx exec. If you do not, use ocx package exec.
ocx package exec is the OCI-tier command. Its first argument is an OCI identifier such as node:20 or ocx.sh/kitware/cmake:3.28@sha256:…. It never reads ocx.toml or ocx.lock, so it behaves identically in every directory and beside any project file. That makes it the right primitive for GitHub Actions, Bazel rules and CI scripts that manage their own tool pins.
ocx exec is the project-tier command. Its symbols are binding names declared in ocx.toml, such as cmake or shellcheck. It resolves those names through ocx.lock and installs missing packages. It then composes the declared environment and spawns the child. A missing ocx.toml is a usage error (exit 64), not a fallback to OCI-tier behavior.
Both commands share these behaviors:
- Install missing packages from the registry.
- Compose and forward the package-declared environment.
- Accept
--cleanto strip inherited shell state. - Accept
--env KEY[:TYPE[:SEP]]=VALUEfor a per-invocation override, or a bare--env NAMEto carry one of the caller’s own variables through. - Forward the child’s exit code byte-for-byte.
Only ocx package exec accepts --self. That flag selects a package’s own private surface, which leaves the package’s entrypoints/ off PATH. Launchers exist for a consumer to invoke the package, while the package’s own runtime calls bin/ directly. A project toolchain is a consumer of every package it declares, so the self view would compose a strictly worse toolchain there.
The two commands cover complementary cases: running a package by its binding name (exec) and running it by its registry identity (package exec). See Project Toolchain In Depth → Running binaries for binary resolution and composition order, and Read the exit code for the codes.
Pin a build that must not move
Section titled “Pin a build that must not move”By default, ocx exec and ocx env resolve a binding through the toolchain’s rendered links/<group>/<entry> link and do not name its package directly. That is a convenience. Run ocx update later, and the same on-disk path keeps resolving. The link’s target moves, and nothing that already reads through it has to recompose.
That convenience is a risk for a build that has to reproduce byte for byte. Suppose a release step records the binary path it resolved. It might write that path to a provenance log, or the artifact might embed an absolute toolchain path in its debug info. That record has to keep naming the one package it was built with. A teammate who runs ocx update in the same checkout for an unrelated binding must not make the recorded path answer with a different binary.
--pinned (or the equivalent pinned = true in ocx.toml) is the escape from the link. It composes the exact digest roots ocx.lock names at that moment and consults no links/<group>/<entry> link. Nothing is left on disk for a later ocx update to move.
# ocx.toml: this project always composes digest paths, whoever runs it.pinned = true
[tools]cmake = "ocx.sh/kitware/cmake:3.28"Export the environment
Section titled “Export the environment”ocx env is the exporter counterpart of ocx exec. It reads ocx.toml and ocx.lock (or the global toolchain under --global) and prints the composed environment instead of running a child. The default is an aligned table, --format json is a document, and --shell[=NAME] prints export lines. Only --shell is safe to eval, and the table and the JSON are not sourceable.
--shell needs the equals form, --shell=bash and not --shell bash, so an unquoted positional token cannot inject shell. A package missing from the object store is installed as part of composition, and one covered by a [[trust.policy]] is signature-verified first. --no-pull skips that fallback and keeps the command strictly offline. Unmaterialised packages are warned about on stderr and omitted, and nothing downloads.
The global tier is lenient. ocx --global env never fails on an unconfigured or corrupt global toolchain and exports an empty environment instead. That rule does not depend on --shell. A corrupt global lock surfaces through the commands that rewrite it, and the project tier stays strict. The installer’s env.sh and env.ps1 call ocx self activate on every shell start. That evaluates ocx --global env --shell=… only when the global toolchain’s activate is env. The nushell hook reads ocx --format json --global env. Global toolchain state cannot break either.
eval "$(ocx env --shell=bash)" # the current project toolchaineval "$(ocx --global env --shell=sh)" # the global toolchainocx env --ci=github # persist to $GITHUB_ENV and $GITHUB_PATHocx env --ci=gitlab >> "${{ export_file }}"--ci=gitlab writes JSON lines for the GitLab step runner’s ${{ export_file }}. That is an experimental path for run: keyword jobs and does not work with traditional script: jobs. CI Integration has a full step-runner example.