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:
| Option | Meaning |
|---|---|
path | Host directories to put on a target's PATH. Empty by default — see The PATH a target sees. |
coreutils | Builtin tools on every target's PATH. true by default; false opts out. |
runner | Workspace-wide default runner for every target using this driver. |
Configuration
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
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:
| Driver | Behavior |
|---|---|
exec | Direct command execution. |
bash | Bash 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:
| Key | Meaning |
|---|---|
run | List of commands to execute. Accepts deferred values. |
deps | Hashed build-time dependencies. |
hash_deps | Hash-only dependencies. |
runtime_deps | Runtime-only dependencies. |
tools | Executables placed on PATH. |
out | Output groups and paths. |
support_files | Non-output artifacts. |
env | Literal environment variables. |
pass_env | Host vars passed at parse time and hashed into the cache key. Accepts ["*"] to pass all. |
runtime_pass_env | Host vars passed at run time only, not hashed. Accepts ["*"] to pass all. |
runtime_env | Runtime literal environment variables. |
cache | True/False toggles local + remote together; dict: {enabled, remote, history}. |
codegen | Write generated outputs into the source tree (copy or in_place). |
runner | Exec 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:
| Variable | Meaning |
|---|---|
SRC / SRC_<group> | Dependency paths. |
LIST_SRC / LIST_SRC_<group> | Dependency list files. |
TOOL / TOOL_<group> | Tool paths. |
OUT / OUT_<group> | Output paths. |
WORKSPACE_ROOT | Root of the workspace. |
PATH | Executables 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.
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:
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):
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:
| Field | Hashed into the key? | Present in the sandbox? | Use for |
|---|---|---|---|
deps | yes | yes | Normal build inputs. |
hash_deps | yes | no | Inputs that must invalidate the cache but the command never reads. |
runtime_deps | no | yes (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:
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:
| Field | Hashed into the cache key? | When applied |
|---|---|---|
pass_env | yes | At parse time. The full environment snapshot is folded into the input hash. |
runtime_pass_env | no | At 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:
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:
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:
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | true | Enable local caching for this target. |
remote | bool | true | Enable remote caching for this target. Set false when outputs embed host-local paths that cannot be safely shared across machines. |
history | int | 1 | Number 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:
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:
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.
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:
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.
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:
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.