Skip to content

lib.modules.env: structured env with prefix/suffix/values/fallback#149

Draft
Lassulus wants to merge 4 commits into
mainfrom
claude/improve-env-fallback-iSFuN
Draft

lib.modules.env: structured env with prefix/suffix/values/fallback#149
Lassulus wants to merge 4 commits into
mainfrom
claude/improve-env-fallback-iSFuN

Conversation

@Lassulus
Copy link
Copy Markdown
Owner

@Lassulus Lassulus commented Apr 8, 2026

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

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
@Lassulus Lassulus marked this pull request as draft April 8, 2026 11:25
claude added 3 commits April 8, 2026 11:40
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
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.

2 participants