feat(plugin-devenv): capture a devenv environment and run targets in it - #413
Open
raphaelvigee wants to merge 2 commits into
Open
feat(plugin-devenv): capture a devenv environment and run targets in it#413raphaelvigee wants to merge 2 commits into
raphaelvigee wants to merge 2 commits into
Conversation
raphaelvigee
marked this pull request as ready for review
August 23, 2026 11:19
raphaelvigee
force-pushed
the
raphaelvigee/exec-runners-devenv
branch
from
August 23, 2026 11:29
9e5aaec to
aa6eec8
Compare
This was referenced Aug 23, 2026
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
force-pushed
the
raphaelvigee/exec-runners-devenv
branch
from
August 23, 2026 17:55
aa6eec8 to
44b94c6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Proved end to end
Against this repo's own devenv, three assertions in both directions:
cargo/nix/storeA 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-envreports 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-envpromises 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-envreports 181 variables. An allowlist of that surface is a permanent maintenance tax that fails closed — a missing entry silently removes a tool. Instead:exportedonly; names dropped forDEVENV_*/NIX_BUILD_*/__*and the per-invocation set; values dropped for naming the tree root,$HOMEor$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/storepaths, andplugin-nixalready 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
pathunder 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:
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 defgains arunnerblock: which runner, and how it was selected (target vsdefaultRunner).heph inspect runnerslists 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.lintclean.Stack created with GitHub Stacks CLI • Give Feedback 💬