- integrations
- Python
- Guides
- Projects & toolchains
Projects & toolchains
Project is the project-tier handle,
obtained from Ocx.project(path) —
never constructed directly. Every call carries --project <path>
explicitly, so no Project method depends on the process’s working
directory, and an ambient OCX_PROJECT can never retarget one (the SDK
neutralizes it on every spawn).
# illustrative: /srv/build stands in for a real project directory.from ocx_sdk import Ocx
project = Ocx().project("/srv/build")project.add("ocx.sh/go-task/task:3", group="ci")project.lock()report = project.env()environment = report.compose().mappingThe toolchain lifecycle
Section titled “The toolchain lifecycle”The lifecycle methods are init, add, remove, lock, update,
pull, status, inspect and env. The
command map lists each with its ocx command.
status exits 0 even when the lock is broken, so read the report. env
returns the composed environment, described below.
lock, update, and pull all accept platform= to resolve against a
target other than the host, and pull/add/lock/update share a
pull: bool | None kwarg that leaves the materialize-or-not choice to ocx’s
own default when omitted.
Env composition: EnvReport → ComposedEnv
Section titled “Env composition: EnvReport → ComposedEnv”Project.env() returns an
EnvReport whose entries are the
toolchain’s [env] contributions, in declaration order.
EnvReport.compose() folds
them into a ComposedEnv:
import os
from ocx_sdk import EnvReport
# ocx always writes all five arrays, so a hand-built payload carries them too.report = EnvReport.from_json( '{"entries": [' '{"key": "JAVA_HOME", "type": "constant", "value": "/opt/jdk"},' '{"key": "PATH", "type": "path", "value": "/opt/jdk/bin"}' '], "binaries": [], "entrypoints": [], "integrations": [], "advisories": []}')composed = report.compose(base={"PATH": "/usr/bin"})assert composed.mapping["JAVA_HOME"] == "/opt/jdk"# PathVar prepends, joined with the platform's own separator:assert composed.mapping["PATH"].split(os.pathsep) == ["/opt/jdk/bin", "/usr/bin"]A report produced through a real Project.env() call carries the handle’s
own host snapshot as compose()’s default base, so composing a report from
a hermetic handle (HostEnv.clean()) stays hermetic — you only pass base=
explicitly to override it, exactly as the example above does.
ComposedEnv has two ways to use the result:
.mapping— a plaindict, the non-invasive form. Pass it straight tosubprocess.run(..., env=...)or any other API that takes an environment mapping. Safe under concurrency..activate()— a context manager that applies the environment toos.environfor the duration of the block. Process-global and single-owner: it changes what every thread and subprocess sees, and a second, overlappingactivate()raisesRuntimeErrorinstead of nesting. The revert on exit is diff-based — only the keys this environment set are restored, absent keys are deleted again, and any unrelated mutation made inside the block survives. Concurrent code should use.mappinginstead.
# illustrative: needs a real EnvReport produced by Project.env().with composed.activate(): subprocess.run(["some-tool"]) # sees the composed environment# os.environ is back to what it was before the block, for exactly the# keys `composed` touched.exec vs spawn
Section titled “exec vs spawn”Both compose ocx exec --project ... [NAMES] -- ARGV under the hood, and
both accept names=, groups=, clean= (strip the ambient parent
environment before composing), env= (extra [env] entries for this call
only), and lazy_mode=.
exec/exec_async— a one-shot: waits, captures (unlesscapture=False), and by default raisesOcxProcessErroron a non-zero child exit.check=Falseis how you inspect a failing build instead of catching an exception. Child processes are never retried, regardless of sessionRetryPolicy— retrying a build step could re-run side effects the first attempt already caused.spawn/spawn_async— starts the command and hands back the livePopen/asyncio.subprocess.Process. No SDK timeout, no output pump: waiting, draining pipes, and killing belong to the caller, exactly like a barePopen.
capture=False on exec inherits stdio and forwards SIGINT to the child —
the shape you want for a long-running build step whose output should stream
straight to the terminal.
Scoping configuration
Section titled “Scoping configuration”with_config(**overrides)
derives a Project that shares this one’s binary, host environment, and
on_log, with only the named OcxConfig fields replaced:
# illustrative: needs a real Project handle.ci_project = project.with_config(offline=True, timeout=30.0)See Errors & credentials for the
credential-scoping pattern this same method enables (with_config(auth={})
after a pull()).