lib.modules.env: structured env with prefix/suffix/values/fallback#149
Draft
Lassulus wants to merge 4 commits into
Draft
lib.modules.env: structured env with prefix/suffix/values/fallback#149Lassulus wants to merge 4 commits into
Lassulus wants to merge 4 commits into
Conversation
Resolves the composability gap from #55 ("Prepend ENV variables") and adds handling for the related cases that came up during that discussion. The old env option was attrsOf str, which forced users into bash parameter-expansion gymnastics whenever they wanted to prefix PATH, provide a fallback default for EDITOR, or unset an inherited variable. The new env option is a submodule per entry with: - value / values: literal or list-of-parts - prefix / suffix: splice around the existing value, makeWrapper style - separator: join separator (default ":") - fallback: only set when the variable isn't already set - unset: emit `unset VAR` List-valued fields merge by concatenation, so modules composing via apply stack contributions cleanly instead of fighting over a single string. Empty/unset env references drop out at runtime via a shared shell helper (_wrapper_env_join), so no dangling separators. Plain strings and null keep working via coercedTo, so existing env.FOO = "bar" / env.FOO = null usage is unchanged. wlib.envRef produces runtime references for the values list; wlib.renderEnvString is exposed for tests and downstream tooling. The systemd integration reads from the new outputs.staticEnv output, which only exposes entries that resolve to a plain literal. https://claude.ai/code/session_01UiEmmBrtkNEstoRemZpnp7
Response to code review — the previous iteration was over-engineered:
too many options, unclear names, helpers scattered across wlib.
Changes:
- Namespace: wlib.envRef / wlib.renderEnvString / normaliseEnvEntry
collapse into wlib.env.{ref, render}. normaliseEnvEntry is now
internal.
- Rename fallback -> ifUnset. "fallback" was vague; "ifUnset" says
what it does: only apply when the caller's env doesn't already
have it set. Now uses ${VAR:-} semantics so empty also counts as
unset.
- Drop prefix / suffix entirely. They were makeWrapper jargon that
wasn't self-evident. The same thing is now expressed via a list
value and wlib.env.ref:
env.PATH.value = [ "/opt/bin" (wlib.env.ref "PATH") ];
- Collapse value + values into a single polymorphic `value` option:
a plain string for the literal case, or a list of parts (joined
with separator) for everything else. One concept, one name.
The module submodule is now just four options: value, separator,
ifUnset, unset. List values still merge by concatenation under
apply, so composition works the same.
lib/modules/env.nix shrinks from 203 to 106 lines; the renderer in
lib/default.nix drops from ~180 to ~120. Tests are consolidated
from four files to three.
https://claude.ai/code/session_01UiEmmBrtkNEstoRemZpnp7
The unset option turned out clunky: combining it with value/ifUnset had silent precedence, and overriding an inherited unset in a later apply required mkForce gymnastics. Unsetting a variable isn't a composable declarative concept, it's a one-line shell command — preHook is right there. Changes: - Remove `env.<VAR>.unset` from the submodule. - Remove the `null` top-level coercion. `env.FOO = null` is now a type error instead of silent unset. - Simplify the renderer: no more unset branch, no more precedence ordering between unset/ifUnset/value. The schema is now three options: value, separator, ifUnset. For unsetting, document `preHook = "unset LD_PRELOAD";` in the README. https://claude.ai/code/session_01UiEmmBrtkNEstoRemZpnp7
Polymorphic `value` (string OR list) meant consumers inspecting another wrapper's env had to safeguard against both types. Now `value` is internally always a list and plain strings coerce to singletons at write time — writing stays ergonomic, reading is uniform. env.EDITOR = "vim"; # coerces to [ "vim" ] a.env.EDITOR.value # => [ "vim" ] (always a list) # Literal-read pattern for consumers: lib.concatStringsSep a.env.EDITOR.separator a.env.EDITOR.value outputs.staticEnv now joins literal-only list values with `separator` instead of rejecting them outright, so `env.FOO = "bar"` still reaches systemd as a plain literal. New checks/env-read.nix exercises the round-trip invariant. https://claude.ai/code/session_01UiEmmBrtkNEstoRemZpnp7
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.
Resolves the composability gap from #55 ("Prepend ENV variables") and
adds handling for the related cases that came up during that
discussion.
The old env option was attrsOf str, which forced users into bash
parameter-expansion gymnastics whenever they wanted to prefix PATH,
provide a fallback default for EDITOR, or unset an inherited variable.
The new env option is a submodule per entry with:
unset VARList-valued fields merge by concatenation, so modules composing via
apply stack contributions cleanly instead of fighting over a single
string. Empty/unset env references drop out at runtime via a shared
shell helper (_wrapper_env_join), so no dangling separators.
Plain strings and null keep working via coercedTo, so existing
env.FOO = "bar" / env.FOO = null usage is unchanged. wlib.envRef
produces runtime references for the values list; wlib.renderEnvString
is exposed for tests and downstream tooling. The systemd integration
reads from the new outputs.staticEnv output, which only exposes
entries that resolve to a plain literal.
https://claude.ai/code/session_01UiEmmBrtkNEstoRemZpnp7