Skip to main content

Exec

The pluginexec plugin provides two execution drivers — exec and bash — for running shell commands inside sandboxed target builds. It wires up dependency injection, sets up environment variables, resolves the tools a target needs on its PATH, and supports interactive shell debugging with PTY allocation. Whenever a target runs a command to produce its outputs, one of these drivers is doing the work.

Driver​

A driver is the component that knows how to execute a target's action and turn its inputs into outputs. This plugin registers the drivers named exec and bash.

Enabling it​

Built-in. Register in .hephconfig under plugins with builtin: exec or builtin: bash. Options:

OptionMeaning
pathHost directories to put on a target's PATH. Empty by default — see The PATH a target sees.
coreutilsBuiltin tools on every target's PATH. true by default; false opts out.
runnerWorkspace-wide default runner for every target using this driver.

Configuration​

.hephconfig
plugins:
- builtin: exec
options:
path: # optional; host directories, none by default
- /usr/local/bin
- /usr/bin
- /bin
coreutils: true # optional; the default
runner: "//tools/devenv:runner" # optional; see Runners
- builtin: bash
options:
path:
- /usr/local/bin
- /usr/bin
- /bin
runner: "//tools/devenv:runner" # optional; see Runners

Usage​

BUILD
target(
name = "hello",
driver = "bash",
run = "echo hello world > $OUT",
out = "greeting.txt",
env = {"MY_VAR": "value"},
pass_env = ["HOME", "USER"],
deps = {"src": "//src:files"},
tools = "//toolchain:gcc",
cache = {"enabled": True, "remote": True, "history": 5},
)

Notes​

There are two driver variants:

DriverBehavior
execDirect command execution.
bashBash shell with job control, run with -o pipefail so a failing stage in a pipeline fails the target.

The bash driver supports an interactive --shell mode with PTY allocation, which is useful for debugging a target's sandbox.

The following target config keys are available:

KeyMeaning
runList of commands to execute. Accepts deferred values.
depsHashed build-time dependencies.
hash_depsHash-only dependencies.
runtime_depsRuntime-only dependencies.
toolsExecutables placed on PATH.
outOutput groups and paths.
support_filesNon-output artifacts.
envLiteral environment variables.
pass_envHost vars passed at parse time and hashed into the cache key. Accepts ["*"] to pass all.
runtime_pass_envHost vars passed at run time only, not hashed. Accepts ["*"] to pass all.
runtime_envRuntime literal environment variables.
cacheTrue/False toggles local + remote together; dict: {enabled, remote, history}.
codegenWrite generated outputs into the source tree (copy or in_place).
runnerExec runner this command runs under — a target address producing a runner.json, or "local" to opt out of a workspace-wide default. See Runners.

These environment variables are available inside the sandbox:

VariableMeaning
SRC / SRC_<group>Dependency paths.
LIST_SRC / LIST_SRC_<group>Dependency list files.
TOOL / TOOL_<group>Tool paths.
OUT / OUT_<group>Output paths.
WORKSPACE_ROOTRoot of the workspace.
PATHExecutables search path.

The PATH a target sees​

A target's PATH holds its tools, any PATH it declares through env, and heph's builtin utilities. The host's directories are not on it. A command that relies on an undeclared host binary fails with command not found instead of passing on whichever machine happens to have it.

BUILD
target(
name = "pack",
driver = "bash",
tools = "//toolchain:tar", # declared: on PATH
run = "tar -cf $OUT $SRC", # declared tool, or a builtin
deps = "//src:files",
out = "pack.tar",
)

If a command needs something else, declare it as a tools entry, call it by absolute path, or list host directories in the driver's path option:

.hephconfig
plugins:
- builtin: bash
options:
path: [/usr/local/bin, /usr/bin, /bin]

The bash driver itself finds bash on the PATH heph was launched with, so you don't need path for the shell. Under a runner, the runner's environment supplies the PATH and path is not applied. The interactive --shell session keeps the host's directories.

Builtin utilities​

heph appends its builtin tools (cp, sed, install, …) to every target's PATH. They fill gaps only: a tool the target declares, or a binary the runner's environment ships, wins over the builtin of the same name. Inside an OCI container they are not available; the image's own utilities apply. Set coreutils: false on the driver to use the host's utilities instead.

Dependencies​

deps accepts either a list or a dict. The dict form names each group, and that name becomes the suffix on the sandbox variables — deps = {"src": …} surfaces as $SRC_SRC (and $LIST_SRC_SRC, a file listing every path in the group, handy when a group expands to many files):

BUILD
target(
name = "bundle",
driver = "bash",
deps = {
"code": glob("*.go"),
"assets": "//web:assets",
},
run = "cat $LIST_SRC_CODE; cp -r $SRC_ASSETS $OUT",
out = "bundle",
)

Group names are uppercased, and any character that isn't a letter, digit, or underscore becomes _ — a group named my-group surfaces as $SRC_MY_GROUP, not $SRC_MY-GROUP (which isn't a valid environment variable name). The same rule applies to out and tools group names.

A plain list lands in the default group, available as $SRC and $LIST_SRC.

heph distinguishes three dependency kinds by how they affect the cache:

FieldHashed into the key?Present in the sandbox?Use for
depsyesyesNormal build inputs.
hash_depsyesnoInputs that must invalidate the cache but the command never reads.
runtime_depsnoyes (at run only)Things needed to run an output, not to build it — changing them is a cache hit.

Output groups​

out mirrors deps: a string or list is the default group ($OUT), a dict names groups ($OUT_<group>). Other targets select a specific group with the |group suffix on the address:

BUILD
target(
name = "compile",
driver = "bash",
run = "go build -o $OUT_BIN .; go doc . > $OUT_DOC",
out = {"bin": "app", "doc": "app.txt"},
)

target(
name = "package",
driver = "bash",
deps = ["//app:compile|bin"], # just the bin group
run = "cp $SRC $OUT",
out = "app.tar",
)

support_files declares files a target produces that are not outputs — kept in the sandbox for the command's own use but not published to dependents.

Output and support-file paths are lexically normalized: ./ prefixes and empty segments are stripped, .. resolves against the package path, and a path that escapes the workspace root is an error.

Environment passthrough​

pass_env and runtime_pass_env each accept a list of variable names to inherit from the host environment. Both also accept the special value "*" to pass through every host environment variable:

FieldHashed into the cache key?When applied
pass_envyesAt parse time. The full environment snapshot is folded into the input hash.
runtime_pass_envnoAt run time only. Variables are not hashed and do not affect the cache.

Because pass_env = ["*"] hashes the entire host environment, any change to any env var forces a rebuild. Only use it on targets where cache is disabled:

BUILD
target(
name = "local-tool",
driver = "exec",
run = ["./build.sh"],
out = "tool",
pass_env = ["*"], # full env snapshot — only for uncached targets
cache = False,
)

Use runtime_pass_env = ["*"] when the command needs the full environment at run time but you still want caching — the environment will not affect the cache key:

BUILD
target(
name = "run-tool",
driver = "exec",
run = ["./tool"],
out = "result",
runtime_pass_env = ["*"], # full env at run time, not hashed
)

Cache control​

cache accepts a bare bool or a dict with up to three keys:

KeyTypeDefaultMeaning
enabledbooltrueEnable local caching for this target.
remotebooltrueEnable remote caching for this target. Set false when outputs embed host-local paths that cannot be safely shared across machines.
historyint1Number of past revisions to retain in the local cache (minimum 1).

A bare True sets both enabled and remote to true. A bare False disables both.

Use cache = {"remote": False} to keep local caching on but opt a specific target out of remote push and pull — useful for targets whose outputs reference paths that only exist on the machine that built them:

BUILD
target(
name = "wrapper",
driver = "exec",
run = ["./gen-wrapper.sh"],
out = "wrapper.sh",
cache = {"remote": False},
)

The target still builds and caches locally; it is never uploaded to or pulled from a remote cache.

Runners​

By default a command runs on this host. Point it at a runner target instead to run inside a described environment — a devenv shell, a container:

BUILD
target(
name = "build",
driver = "bash",
run = "make",
out = "out/",
runner = "//tools/devenv:runner",
)

Set runner on the driver's own options (see Configuration) to apply it to every exec/bash target in the workspace by default; a target's own runner field overrides that default, and runner = "local" opts a single target back onto the host. See Runners for the full picture, including what environment a target actually sees.

Deferred values​

run, and the values of env and runtime_env, can reference another target's output instead of a literal — ${read://pkg:name} for its contents, ${src://pkg:name} for its sandbox path — and heph resolves the reference once that target has run. See Deferred values.

BUILD
target(
name = "deploy",
driver = "bash",
run = 'echo "deploying $VERSION"',
env = {"VERSION": "${read://:version}"},
)

Builtin coreutils​

heph ships 40 common POSIX utilities in its own binary. With the toolbox on, they are available on every target's PATH, so cp, install or sha256sum behave the same on Linux and macOS instead of resolving to GNU or BSD variants.

It is off by default. Enable it per driver:

.hephconfig
plugins:
- builtin: exec
options:
coreutils: true
- builtin: bash
options:
coreutils: true

Included: base64, basename, cat, chmod, comm, cp, cut, date, dirname, echo, env, false, head, install, ln, md5sum, mkdir, mktemp, mv, nproc, printf, readlink, realpath, rm, rmdir, seq, sha1sum, sha256sum, sha512sum, sleep, sort, stat, tail, tee, timeout, touch, tr, true, uniq, wc.

The builtins fill gaps, they don't override anything: they come last on PATH, after the target's tools, its declared dependencies and the runner's environment. A tool you provide yourself always wins. Targets under a container runner (such as oci) use the image's own utilities.

warning

Turning the toolbox on changes the cache key of every exec and bash target. A new heph release that changes the toolbox does so again.

Inspect the set from the CLI:

heph tool coreutils list # the bundled utilities and the toolbox version
heph tool coreutils which cp # is `cp` builtin, and what the host provides instead
heph tool coreutils run cp -r a b # run one directly, outside a sandbox

Interactive debugging with --shell​

When a target fails, drop into its sandbox with the exact inputs, tools, and environment it would run with:

terminal
heph run //app:server --shell

The bash driver allocates a PTY and starts an interactive shell inside the prepared sandbox, so you can inspect $SRC_*, re-run the command by hand, and see why it broke.