From d1eae0561df606a61f99c3f1021d01ffba21078b Mon Sep 17 00:00:00 2001 From: losnappas Date: Sat, 8 Nov 2025 14:49:55 +0200 Subject: [PATCH] env.nix: merge multiple declarations of env vars --- .gitignore | 2 + shell-modules/env.nix | 88 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cc724a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.direnv +testing diff --git a/shell-modules/env.nix b/shell-modules/env.nix index dd15db5..08cac48 100644 --- a/shell-modules/env.nix +++ b/shell-modules/env.nix @@ -1,6 +1,15 @@ { config, lib, ... }: let inherit (lib) mkOption types; + singleType = types.nullOr ( + types.oneOf [ + types.bool + types.int + types.package + types.path + types.str + ] + ); in { options = { @@ -10,6 +19,9 @@ in An attribute set to control environment variables in the shell environment. If the value of an attribute is `null`, the variable of that attribute's name is `unset`. Otherwise the variable of the attribute name is set to the attribute's value. Integer, path, and derivation values are converted to strings. The boolean true value is converted to the string `"1"`, and the boolean false value is converted to the empty string `""`. + + Multiple declarations for the same environment variable are allowed. + The merge strategy can be configured via the `envStrategies` option. ''; example = lib.literalExpression '' { @@ -21,18 +33,70 @@ in COWSAY = pkgs.cowsay } ''; + type = types.attrsOf (types.coercedTo singleType (v: [ v ]) (types.listOf singleType)); + }; + + envStrategies = mkOption { + default = { + LD_LIBRARY_PATH = { + strategy = "append"; + separator = ":"; + }; + }; + description = "Merge strategies for environment variables with multiple definitions."; + example = lib.literalExpression '' + { + PATH = { + strategy = "append"; + separator = ":"; + }; + } + ''; type = types.attrsOf ( - types.nullOr ( - types.oneOf [ - types.bool - types.int - types.package - types.path - types.str - ] - ) + types.submodule { + options = { + strategy = mkOption { + type = types.enum [ + "append" + "error" + ]; + default = "error"; + description = "Merge strategy: 'append' or 'error' (default)."; + }; + separator = mkOption { + type = types.str; + default = ":"; + description = "Separator for 'append' strategy."; + }; + }; + } ); }; + + mergedEnv = mkOption { + internal = true; + readOnly = true; + default = lib.mapAttrs ( + name: values: + let + cfg = config.envStrategies.${name} or { }; + strategy = cfg.strategy or "error"; + separator = cfg.separator or ":"; + isUnset = (lib.last values) == null; + definedValues = lib.filter (v: v != null) values; + in + if isUnset then + null + else if builtins.length definedValues > 1 && strategy == "error" then + throw "The environment variable '${name}' has multiple definitions. Please specify a merge strategy using 'envStrategies.${name}.strategy'." + else if strategy == "append" then + lib.concatStringsSep separator (map builtins.toString definedValues) + # strategy is "error" with 0 or 1 value + else + lib.last definedValues + ) config.env; + }; + finalEnv = mkOption { readOnly = true; internal = true; @@ -50,8 +114,8 @@ in let inherit (builtins) isPath toString; inherit (lib.attrsets) filterAttrs mapAttrs; - simpleEnv = filterAttrs (_: v: !(v == null || isPath v)) config.env; - pathEnv = filterAttrs (_: isPath) config.env; + simpleEnv = filterAttrs (_: v: !(v == null || isPath v)) config.mergedEnv; + pathEnv = filterAttrs (_: isPath) config.mergedEnv; in simpleEnv // mapAttrs (_: toString) pathEnv; }; @@ -61,7 +125,7 @@ in inherit (builtins) attrNames concatStringsSep; inherit (lib) mkIf; inherit (lib.attrsets) filterAttrs; - envVarsToUnset = attrNames (filterAttrs (_: v: v == null) config.env); + envVarsToUnset = attrNames (filterAttrs (_: v: v == null) config.mergedEnv); in mkIf (envVarsToUnset != [ ]) { shellHook = "unset ${concatStringsSep " " envVarsToUnset}";