- docs
- Publishing
- Add named launchers
Add named launchers
By the end of this page your package declares named launchers. OCX generates one small script per name at install time. Each script lands on the consumer’s PATH as a bare command once the consumer selects the package with ocx package select or ocx package install --select. The launcher runs the command in the package’s own environment, so two packages that share a runtime stop fighting over one ambient interpreter.
You need a package with a bin/ directory and, for scripted tools, a dependency that provides the interpreter. The concept page explains how launchers work, and the field reference lists every key.
Why encapsulate through a launcher
Section titled “Why encapsulate through a launcher”Say two of your packages are JavaScript tools that depend on Node, and two more are Java tools that depend on a JDK. A consumer who exposes all four bin/ directories gets one inherited node and one java, whichever the shell finds first. Upgrade Node and one tool breaks. Use the wrong JDK and another fails silently.
An entry point cuts the exposure down to the launcher alone. The launcher re-enters through ocx launcher exec, an internal entry point hidden from help that the concept page describes. It composes the package’s private surface on top of the shell environment. That surface holds the package’s own env entries plus those of its declared dependencies. The pinned Node.js from dependencies[] wins on the composed PATH, and it never appears as a bare entry on the consumer’s shell.
When to declare entry points
Section titled “When to declare entry points”Declare entrypoints when one of these is true:
- Your package depends on a runtime another package also uses. Python scripts, Node.js CLIs, JVM tools and Ruby gems are meaningless without a specific interpreter on PATH.
- Your package must find a dependency at run time. Declare the dependency with
visibility: privateand use${deps.NAME.installPath}in anenvvalue. The launcher resolves the name against that PATH. - The tool must run with the env you declared. PATH exposure cannot do that, because the launched binary inherits whatever the consumer’s shell carried in.
- Exposing
bin/would leak too much. A toolchain with fifty binaries can declare three entry points and keepbin/private.
Skip entry points for self-contained static binaries such as Go or Rust builds. CMake, ripgrep and mold have no runtime to conflict over, so a public ${installPath}/bin on PATH is enough.
A package with entry points usually demotes ${installPath}/bin from public to private (see declare the environment). OCX prepends each installed package’s entrypoints/ directory to PATH at exec time, and consumers see it as <symlink-root>/current/entrypoints.
Pick names that do not collide
Section titled “Pick names that do not collide”The name is what consumers type at the shell, so it should read like a top-level command. The reference gives the exact name grammar. cmake-gen is fine, and __internal-helper is rejected.
OCX checks for collisions at install time and when ocx package exec or ocx env receives two or more roots. It reports an EntrypointCollision error and never picks one silently. Three habits avoid the error:
- Match the upstream binary name when you wrap a single upstream binary. If you ship CMake, declare
cmake,ctestandcpack. - Namespace internal launchers. A wrapper for
myorg/build-toolsshould declaremyorg-buildormbt, not a genericbuild. - Search the package catalog before you publish a public package. Names that upstream projects already use are the high-risk set.
Entry points in depth covers the error format and the collision rules.
Point the launcher at the binary
Section titled “Point the launcher at the binary”Each key of entrypoints is a command name, and the value is {} in the common case. The launcher searches the composed PATH for the key. That search honors PATHEXT on Windows, so a packaged tool.bat or tool.cmd resolves by bare name. Declare the binary’s location once through env, and every entry name picks it up. The reference lists the optional command and args fields.
To wrap a single bundled binary, put bin/ on the PATH and declare the name:
{ "env": [ { "key": "PATH", "type": "path", "value": "${installPath}/bin", "visibility": "private" } ], "entrypoints": { "cmake": {} }}To expose a binary from a dependency without re-bundling it, put the dependency’s bin/ on the composed PATH:
{ "dependencies": [ { "identifier": "ocx.sh/kitware/cmake:3.28@sha256:abc...", "name": "cmake", "visibility": "public" } ], "env": [ { "key": "PATH", "type": "path", "value": "${deps.cmake.installPath}/bin", "visibility": "private" } ], "entrypoints": { "cmake": {} }}Bake leading arguments
Section titled “Bake leading arguments”Use args to put fixed arguments in front of the ones the user types. The launcher prepends them on every call. Use command when the dispatched binary has another name than the launcher:
{ "entrypoints": { "mytool": { "command": "python", "args": ["${installPath}/app/main.py"] } }}Running mytool a b runs python <content>/app/main.py a b.
- Each
argselement is one argv token. There is no word splitting, so a path with spaces stays one argument. ${installPath}works inargs, with its render modifiers.${deps.*}and${self.env.*}are refused at publish time.commandis a plain name, never a path, and it takes no interpolation. The launcher resolves it on the composedPATH.- To run a dependency’s binary, put that dependency’s
bin/on the composedPATHthroughenv, as in the dependency example above.
Worked example: a Python script with a pinned interpreter
Section titled “Worked example: a Python script with a pinned interpreter”Take mytool, a Python CLI whose only file is bin/mytool:
#!/usr/bin/env python3import sysprint("mytool", "running on", sys.version)The shebang resolves python3 from PATH at exec time. With bin/ exposed as public, that is whichever Python the consumer’s shell finds first. A system Python, a mise-managed one or another OCX package’s Python can all win. Declare CPython as a private dependency, declare the script as an entry point, and demote bin/ to private:
{ "type": "bundle", "version": 1, "dependencies": [ { "identifier": "ocx.sh/cpython:3.13@sha256:abc...", "name": "cpython", "visibility": "private" } ], "env": [ { "key": "PATH", "type": "path", "value": "${installPath}/bin", "visibility": "private" } ], "entrypoints": { "mytool": {} }}At exec time this happens:
- The consumer types
mytool, and the launcher inentrypoints/runs. - The launcher re-enters through
ocx launcher exec. OCX composes the private env: the cpython PATH first, then this package’sbin/on top. - OCX runs
bin/mytool. The shebang resolvespython3against the composed PATH and finds the pinned CPython.
The consumer never sees python3 on their shell PATH. Keep the dependency edge private, because consumers should not have to learn that the package uses Python. Use public only when consumers should compose the dependency themselves.
Declare the executables
Section titled “Declare the executables”Tools such as CI matrices and Bazel toolchain rules want to know whether cmake resolves on PATH without installing the package. The optional binaries array answers that with bare names. List the names that an interface-visible path variable puts on PATH:
{ "env": [ { "key": "PATH", "type": "path", "value": "${installPath}/bin", "visibility": "public" } ], "binaries": ["cmake", "cpack", "ctest"]}The array is a claim, and OCX does not verify it. ocx package push streams layers without unpacking them, and a package can reuse layers by digest. No step ever holds the full content tree, so a check against disk would break the streaming model. Other ecosystems make the same trade: npm’s bin, Cargo’s [[bin]] and nixpkgs’ meta.mainProgram.
ocx package create with --bin-scan fills or checks the claim against the tree it can see. Declare layers added at push time by hand. The reference has the name grammar.
An omitted binaries means nobody declared the field, and "binaries": [] asserts that the package puts no executables on PATH. Never list a launcher name that only exists as an entry point. binaries names files you shipped, and entrypoints names launchers OCX generates. The same name may appear in both, as with cmake in the examples above. The reference explains the difference.
See also
Section titled “See also”- Entry points reference for every field and constraint
- Executables reference for the
binariesfield and its name grammar - Entry points in depth for launcher mechanics, synth-PATH and multi-owner reporting
- Declare the environment for demoting
bin/from public to private selectcollisions for detection at select time