nushell: load session environment at runtime

Source the OS and Home Manager POSIX environment scripts when Nushell
starts so runtime HOME and USER expansions and profile paths match managed
POSIX shells.
This commit is contained in:
Lisa Scheers
2026-10-22 16:05:41 -05:00
committed by Austin Horstman
parent 2f5ab4a8fe
commit 6750249c69
5 changed files with 283 additions and 1 deletions
+9
View File
@@ -332,6 +332,15 @@ The state version in this release includes the changes below. These
changes are only active if the `home.stateVersion` option is set to
"26.11" or later.
- [](#opt-programs.nushell.loadSessionVariables) defaults to `true` from
`home.stateVersion = "26.11"`. Nushell loads the Home Manager session
environment at startup and, when used as a NixOS or nix-darwin module,
the system environment too. This makes Home Manager manage `env.nu`;
migrate independently managed contents to [](#opt-programs.nushell.envFile)
or [](#opt-programs.nushell.extraEnv), or disable the option.
Earlier state versions retain the old behavior and emit an evaluation
warning until `programs.nushell.loadSessionVariables` is explicitly set.
- On Darwin, the default value of
[](#opt-programs.firefox.configPath) changes from
`Library/Application Support/Firefox` to
+99 -1
View File
@@ -1,6 +1,7 @@
{
config,
lib,
osConfig,
pkgs,
...
}:
@@ -9,6 +10,72 @@ let
inherit (lib.hm.nushell) isNushellInline toNushell;
cfg = config.programs.nushell;
systemEnvironmentFile = osConfig.system.build.setEnvironment or null;
hasSystemEnvironment = systemEnvironmentFile != null;
homeSessionVariablesFile = "${config.home.sessionVariablesPackage}/etc/profile.d/hm-session-vars.sh";
# Translate the POSIX session scripts at shell startup so references to
# runtime values such as HOME and USER are expanded in the user environment.
sessionVariablesLoader =
let
sourceSystemEnvironment = lib.optionalString hasSystemEnvironment ''
if [ -z "''${__NIXOS_SET_ENVIRONMENT_DONE-}" ] \
&& [ -z "''${__NIX_DARWIN_SET_ENVIRONMENT_DONE-}" ]; then
. ${lib.escapeShellArg systemEnvironmentFile} >/dev/null
fi
'';
# A script avoids Bash's SSH-specific bashrc loading for `bash -c`.
captureEnvironment = pkgs.writeShellScript "hm-session-vars-capture" ''
${lib.getExe' pkgs.coreutils "env"} -0
printf '\0'
${sourceSystemEnvironment}
. ${lib.escapeShellArg homeSessionVariablesFile} >/dev/null
${lib.getExe' pkgs.coreutils "env"} -0
'';
shouldLoadSystemEnvironment = lib.optionalString hasSystemEnvironment ''
or (
($env.__NIXOS_SET_ENVIRONMENT_DONE? | is-empty)
and ($env.__NIX_DARWIN_SET_ENVIRONMENT_DONE? | is-empty)
)
'';
in
pkgs.writeText "hm-session-vars.nu" ''
if (
($env.__HM_SESS_VARS_SOURCED? | is-empty)
${shouldLoadSystemEnvironment}
) {
let sections = (
^${captureEnvironment}
| split row $"(char nul)(char nul)"
| each {
split row (char nul)
| parse --regex '(?s)^(?<name>[^=]+)=(?<value>.*)$'
| transpose --header-row --as-record
}
)
let before = $sections.0
let after = $sections.1
let ignored = ["_" "_AST_FEATURES" "SHLVL"]
let changed = ($after | items { |name, value|
if $name not-in $ignored and (
$name not-in $before or ($before | get $name) != $value
) {
[$name $value]
}
} | compact | into record)
load-env $changed
if "PATH" in $changed {
$env.PATH = $changed.PATH | split row (char esep)
}
for name in ($before | columns) {
if $name not-in $after and $name not-in $ignored {
hide-env --ignore-errors $name
}
}
}
'';
linesOrSource =
name:
types.submodule (
@@ -191,6 +258,36 @@ in
'';
};
loadSessionVariables = lib.mkOption {
type = types.bool;
inherit
(lib.hm.deprecations.mkStateVersionOptionDefault {
inherit (config.home) stateVersion;
since = "26.11";
optionPath = [
"programs"
"nushell"
"loadSessionVariables"
];
legacy.value = false;
current.value = true;
})
default
defaultText
;
description = ''
Whether to load the Home Manager session environment, such as
{option}`home.sessionVariables` and {option}`home.sessionPath`, in
{file}`env.nu` when Nushell starts without it, for example as a login
shell. When Home Manager is used as a NixOS or nix-darwin module, the
system environment is loaded too.
Enabling this makes Home Manager manage {file}`env.nu`. Put other
environment configuration in {option}`programs.nushell.envFile` or
{option}`programs.nushell.extraEnv`.
'';
};
environmentVariables = lib.mkOption {
type = types.attrsOf lib.hm.types.nushellValue;
default = { };
@@ -279,8 +376,9 @@ in
}
)
(lib.mkIf (cfg.envFile != null || cfg.extraEnv != "") {
(lib.mkIf (cfg.loadSessionVariables || cfg.envFile != null || cfg.extraEnv != "") {
"${cfg.configDir}/env.nu".text = lib.mkMerge [
(lib.mkIf cfg.loadSessionVariables "source ${sessionVariablesLoader}")
(lib.mkIf (cfg.envFile != null) cfg.envFile.text)
cfg.extraEnv
];
@@ -1,4 +1,19 @@
{
nushell-example-settings = ./example-settings.nix;
nushell-config-dir = ./config-dir.nix;
nushell-session-variables = ./session-variables.nix;
nushell-session-variables-legacy = import ./session-variables-state-version.nix {
stateVersion = "26.05";
};
nushell-session-variables-current = import ./session-variables-state-version.nix {
stateVersion = "26.11";
};
nushell-session-variables-opt-in = import ./session-variables-state-version.nix {
stateVersion = "26.05";
loadSessionVariables = true;
};
nushell-session-variables-opt-out = import ./session-variables-state-version.nix {
stateVersion = "26.11";
loadSessionVariables = false;
};
}
@@ -0,0 +1,30 @@
{
stateVersion,
loadSessionVariables ? null,
}:
{ lib, ... }:
{
home.stateVersion = stateVersion;
programs.nushell = {
enable = true;
}
// lib.optionalAttrs (loadSessionVariables != null) { inherit loadSessionVariables; };
nmt.script =
let
enabled =
if loadSessionVariables != null then
loadSessionVariables
else
lib.versionAtLeast stateVersion "26.11";
envFile = "home-files/.config/nushell/env.nu";
in
if enabled then
''
assertFileRegex ${envFile} '^source /nix/store/[^/]*-hm-session-vars.nu$'
''
else
''
assertPathNotExists ${envFile}
'';
}
@@ -0,0 +1,130 @@
{
config,
lib,
pkgs,
realPkgs,
...
}:
let
systemEnvironment = pkgs.writeText "set-environment" ''
if [ -n "''${SKIP_SYSTEM-}" ]; then return; fi
export __NIXOS_SET_ENVIRONMENT_DONE=1
export SYSTEM_VALUE="$HOME/$USER"
export PATH="/system/bin:$PATH"
unset REMOVED_BY_SYSTEM
'';
in
{
_module.args.osConfig.system.build.setEnvironment = systemEnvironment;
home = {
stateVersion = "26.11";
sessionPath = [ "$HOME/.local/bin" ];
sessionVariables = {
ALTERNATE_VALUE = "\${HOME:+home-is-set}";
DEFAULT_VALUE = "\${UNSET_VARIABLE:-fallback}";
EXPANDED_HOME = "$HOME/.config";
};
};
programs.nushell = {
enable = true;
package = realPkgs.nushell;
envFile.text = ''
$env.USER_ENV_FILE = "env-file-loaded"
'';
};
nmt.script =
let
homePrefix = "${config.home.homeDirectory}/";
configDir = lib.removePrefix homePrefix config.programs.nushell.configDir;
envFile = "home-files/${configDir}/env.nu";
in
''
assertFileRegex ${envFile} \
'^source /nix/store/[^/]*-hm-session-vars.nu$'
for ssh_client in "" "10.0.0.1 5000 22"; do
env -i \
HOME=/runtime/home \
USER=runtime-user \
PATH=/initial/bin \
REMOVED_BY_SYSTEM=inherited \
SSH_CLIENT="$ssh_client" \
${lib.getExe realPkgs.nushell} \
--login \
--no-history \
--config /dev/null \
--env-config "$(_abs ${envFile})" \
--commands '
use std/assert
assert equal $env.SYSTEM_VALUE /runtime/home/runtime-user
assert equal $env.EXPANDED_HOME /runtime/home/.config
assert equal $env.DEFAULT_VALUE fallback
assert equal $env.ALTERNATE_VALUE home-is-set
assert equal $env.PATH [/runtime/home/.local/bin /system/bin /initial/bin]
assert equal $env.USER_ENV_FILE env-file-loaded
assert ("REMOVED_BY_SYSTEM" not-in $env)
' || fail "Nushell did not load the session environment."
# tmux on nix-darwin clears the marker to request reinitialization.
for marker in __NIX_DARWIN_SET_ENVIRONMENT_DONE \
__NIXOS_SET_ENVIRONMENT_DONE; do
env -i \
HOME=/runtime/home \
USER=runtime-user \
PATH=/initial/bin \
__HM_SESS_VARS_SOURCED=1 \
"$marker=" \
REMOVED_BY_SYSTEM=inherited \
SSH_CLIENT="$ssh_client" \
${lib.getExe realPkgs.nushell} \
--login \
--no-history \
--config /dev/null \
--env-config "$(_abs ${envFile})" \
--commands '
use std/assert
assert equal $env.SYSTEM_VALUE /runtime/home/runtime-user
assert equal $env.PATH [/system/bin /initial/bin]
assert ("EXPANDED_HOME" not-in $env)
assert ("REMOVED_BY_SYSTEM" not-in $env)
' || fail "Nushell did not reload with an empty $marker."
done
# Reloading scripts that leave the environment unchanged is valid too.
env -i \
HOME=/runtime/home USER=runtime-user PATH=/initial/bin \
__HM_SESS_VARS_SOURCED=1 SKIP_SYSTEM=1 \
SSH_CLIENT="$ssh_client" \
${lib.getExe realPkgs.nushell} \
--login --no-history --config /dev/null \
--env-config "$(_abs ${envFile})" \
--commands '
use std/assert
assert equal $env.PATH [/initial/bin]
assert equal $env.USER_ENV_FILE env-file-loaded
' || fail "Nushell failed with an unchanged session environment."
done
# Model a remote Bash startup file that initializes the system first.
mkdir -p ssh-home
printf '%s\n' '. ${systemEnvironment}' > ssh-home/.bashrc
for hm_marker in "" 1; do
env -i \
HOME="$PWD/ssh-home" USER=runtime-user PATH=/initial/bin \
SSH_CLIENT="10.0.0.1 5000 22" \
__HM_SESS_VARS_SOURCED="$hm_marker" \
__NIX_DARWIN_SET_ENVIRONMENT_DONE= \
${lib.getExe realPkgs.nushell} \
--login --no-history --config /dev/null \
--env-config "$(_abs ${envFile})" \
--commands '
use std/assert
assert equal $env.SYSTEM_VALUE ($env.HOME + "/runtime-user")
assert equal $env.__NIXOS_SET_ENVIRONMENT_DONE "1"
' || fail "Nushell lost the system environment over SSH."
done
'';
}