Skip to content

feat(plugin-devenv): capture a devenv environment and run targets in it - #413

Open
raphaelvigee wants to merge 2 commits into
raphaelvigee/exec-runners-phase2from
raphaelvigee/exec-runners-devenv
Open

feat(plugin-devenv): capture a devenv environment and run targets in it#413
raphaelvigee wants to merge 2 commits into
raphaelvigee/exec-runners-phase2from
raphaelvigee/exec-runners-devenv

Conversation

@raphaelvigee

@raphaelvigee raphaelvigee commented Aug 22, 2026

Copy link
Copy Markdown
Member

Phase 1c of #404, on top of #411. The half you actually asked for: a target runs inside the devenv.sh environment and inherits its tools.

target(name = "env", driver = "devenv")
target(name = "build", driver = "bash", run = ["cargo build"], runner = "//:env")

Proved end to end

Against this repo's own devenv, three assertions in both directions:

target under the runner finds cargo PASS
same target without it does not PASS
every PATH entry it sees is /nix/store PASS

A correction to the evidence behind the store-only rule

The original measurement came from the live interactive shell's $PATH — 107 entries, 38 outside /nix/store. Right worry, wrong source. print-dev-env reports the derivation's environment: on the same machine, 66 PATH entries, all store paths. The login PATH the shell appends simply isn't in it.

The store-only rule stays anyway, because it's a guarantee, not a repair: nothing about print-dev-env promises it, and what it prevents is silent — a mutable host directory in the snapshot means two machines with identical PATH strings and different tools produce identical cache keys.

Filtering: three rules, not an allowlist

print-dev-env reports 181 variables. An allowlist of that surface is a permanent maintenance tax that fails closed — a missing entry silently removes a tool. Instead: exported only; names dropped for DEVENV_*/NIX_BUILD_*/__* and the per-invocation set; values dropped for naming the tree root, $HOME or $TMPDIR.

Against the real output that keeps 99 variables and drops exactly one on the value rule — shellHook, which embeds the checkout path. Frozen by a test asserting two different checkout paths give byte-identical snapshots, which is what keeps a laptop and CI on the same cache key.

Snapshots are local-cache only: the PATH is host-local /nix/store paths, and plugin-nix already refuses to share the same kind of artifact for the same reason.

Fixes a rule that was specified and never implemented

A session's PATH is supposed to replace the driver's rather than sit under it. It didn't — the driver's default was still winning, so the runner's PATH never took effect. The very first end-to-end run failed on exactly this.

It matters because with the driver's default appended, a tool missing from the environment falls through to the host — the ambient dependency a runner exists to remove — under a cache key asserting the runner's environment. An explicitly written path under a runner is now a hard error rather than a silent discard. Regression-tested end to end.

Snapshot mode's limitation is now recoverable

The snapshot records the environment's shell-function names, so a target calling one gets:

fmt-all is a shell function, not a binary on PATH — set mode = "session"

instead of "not found in PATH", which sends the reader hunting for a package that isn't missing. (Session mode itself lands in #414.)

Diagnostics

  • heph inspect def gains a runner block: which runner, and how it was selected (target vs defaultRunner).
  • heph inspect runners lists what the workspace can use.

Both static by design — every invocation owns its own session pool, so a separate process cannot see live sessions and would render an empty table that reads as "nothing is running".

Tests

7 unit (snapshot filtering, incl. the two-checkouts property and the real JSON shape) + 1 new e2e for the PATH-replacement rule, on top of the 10 already in exec_runner.rs. Full local run green: 545 e2e, plus engine/exec-runner/plugin-exec/heph. lint clean.

Stack created with GitHub Stacks CLIGive Feedback 💬

raphaelvigee and others added 2 commits August 23, 2026 19:52
Phase 1c of docs/EXEC_RUNNERS.md — the half a user actually asks for. A
`devenv` target captures the devenv.sh environment as an artifact, and targets
naming it as their `runner` are created in that environment.

Proved end to end against this repo's own devenv, three ways: a target under
the runner finds `cargo`; the same target without the runner does not; and
every entry of the PATH it sees is a `/nix/store` path.

Two halves under one name, because that is how a runner is selected — by the
driver name of the runner target. The driver spawns `devenv print-dev-env
--json` and writes a canonicalized snapshot; the runner does nothing but parse
that snapshot back. Keeping the runner a pure parse is what makes it sound:
`open` runs after `hashin` and not at all on a fully-cached build, so anything
it discovered would be unhashed input that cannot be checked on the build where
a stale artifact is served.

**A correction to the design's own evidence.** §5 was written from a
measurement of the live interactive shell's `$PATH` — 107 entries, 38 outside
`/nix/store`. That is the right worry about the wrong source. `print-dev-env`
reports the *derivation's* environment, and on the same machine it yields 66
PATH entries, all of them store paths; the login PATH the shell appends is
simply not in it. The store-only rule stays regardless, because it is a
guarantee rather than a repair: nothing about `print-dev-env` promises it, and
what it prevents is silent — a mutable host directory in the snapshot means two
machines with identical PATH strings and different tools produce identical
cache keys.

Filtering is three rules rather than the flat allowlist first sketched, because
`print-dev-env` reports 181 variables and an allowlist of that surface would be
a permanent maintenance tax that fails *closed* — a missing entry silently
removes a tool. Only `exported` variables; names dropped for `DEVENV_*` /
`NIX_BUILD_*` / `__*` and the per-invocation set; values dropped for naming the
tree root, `$HOME` or `$TMPDIR`. Against the real output that keeps 99
variables and drops exactly one on the value rule — `shellHook`, which embeds
the checkout path. Frozen by a test asserting two different checkout paths give
byte-identical snapshots, which is the property that keeps a laptop and CI on
the same cache key.

Snapshots are local-cache only. The PATH is a list of host-local `/nix/store`
paths, and `plugin-nix` already refuses to share the same kind of artifact for
the same reason.

**Also fixes a rule that was documented and never implemented.** §4.4 says a
session's PATH *replaces* the driver's rather than sitting under it; the driver
was still winning, so the runner's PATH never took effect — the first
end-to-end run failed on it. With the driver's default appended, a tool missing
from the environment falls through to the host: the exact ambient dependency a
runner removes, under a key asserting the runner's environment. An explicitly
written `path` under a runner is now a hard error rather than a silent discard.
Regression-tested end to end.

M1's one real limitation is now recoverable rather than a dead end: the
snapshot records the environment's shell-function *names*, so a target calling
one gets "`fmt-all` is a shell function, not a binary on PATH — set `mode =
\"session\"`" instead of a "not found in PATH" that sends the reader hunting
for a package that is not missing.

Diagnostics (§6): `heph inspect def` gains a `runner` block naming the runner
and how it was selected, and `heph inspect runners` lists what the workspace
can use. Deliberately static — every invocation owns its own session pool, so
a separate process cannot see live sessions and would render an empty table
that reads as "nothing is running".
This commit adds `shell_functions` to `SessionDescription`; the go e2e harness
constructs one, so it declares the new field. Diagnostics only, and the
recording runner is the identity, so an empty list is the honest answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RC5ykWRMPjRKGk1Tz1KTja
@raphaelvigee
raphaelvigee force-pushed the raphaelvigee/exec-runners-devenv branch from aa6eec8 to 44b94c6 Compare August 23, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant