Fixing PATH in fish with nix-darwin
Sharing how I fixed a PATH order issue when using Fish shell with nix-darwin. Since Fish reimplements macOS's path_helper behavior, it was reordering PATH and putting .nix-profile/bin at the end. By remembering the original PATH before Fish's initialization and restoring the order afterward, I got nix packages to properly override system binaries. I've included detailed debugging steps using Fish's event handlers in case others run into similar issues.
During migration to nix
for package and system management in environment#11, I've encountered an issue with PATH
variable containing seemingly correct entries, but in incorrect order when using fish
. Basically, $HOME/.nix-profile/bin
is put in the end. Since I am very new to nix
ecosystem (using it for few days), it was not clear what is causing this issue (my configuration, nix-home-manager
, nix-darwin
or fish
itself), so I decided to investigate. While it turned out to be a known issue, I learned a little bit in the process and found a local fix, which I am sharing in the end of the post.
When bash
is set as user shell, the value of PATH
is good expect for repeating values.
$ echo $PATH /Users/d12frosted/.nix-profile/bin: \ /nix/var/nix/profiles/default/bin: \ /usr/local/bin: \ /usr/bin: \ /bin: \ /usr/sbin: \ /sbin: \ /Library/TeX/texbin: \ /usr/local/MacGPG2/bin: \ /opt/X11/bin: \ /Library/Apple/usr/bin: \ /Users/d12frosted/.nix-profile/bin: \ /run/current-system/sw/bin: \ /nix/var/nix/profiles/default/bin
The important part here is $HOME/.nix-profile/bin
being the first item in this list, which is expected and desired, because we want binaries installed via package manager (in this case nix
) to shadow any built-in binaries (common with coreutils
package).
But when fish
is used as user shell, the list doesn't contain any duplicates, but $HOME/.nix-profile/bin
and /nix/var/nix/profiles/default/bin
are placed in the end.
$ echo $PATH /usr/local/bin \ /usr/bin \ /bin \ /usr/sbin \ /sbin \ /opt/X11/bin \ /Library/Apple/usr/bin \ /usr/local/MacGPG2/bin \ /Library/TeX/texbin \ /Users/d12frosted/.nix-profile/bin \ /run/current-system/sw/bin \ /nix/var/nix/profiles/default/bin
And I become curious about reasons behind difference of these values and possible solution. Since I am very new to nix
ecosystem, I decided to start with something more familiar - fish
. It turns out, that it's possible to debug variable modifications with fish
by using event handlers, which we can put somewhere in the very beginning of initialization process, which is $__fish_data_dir/config.fish
- configuration file shipped with fish
itself that is loaded first. In general no one should ever modify this file, but we are debugging, so it's fine. I also use status function to display extra information (mostly interested in stack trace).
# Add these lines to the very beginning of $__fish_data_dir/config.fish # /nix/store/r5brs3gn4amxbl1mrl4433inlghwl1r0-fish-3.2.2/share/fish/config.fish echo "PATH before initialisation > $PATH" function __notice_path_change -d "Notice PATH changes" --on-variable PATH echo "PATH has changed to $PATH" status end
After firing a new fish
session, I see the following output in terminal emulator.
PATH before initialisation > /Users/d12frosted/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin:/Users/d12frosted/.config/bin:/Users/d12frosted/.local/bin PATH has changed to /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Library/Apple/usr/bin:/usr/local/MacGPG2/bin:/Library/TeX/texbin:/Users/d12frosted/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/Users/d12frosted/.config/bin:/Users/d12frosted/.local/bin This is a login shell Job control: Only on interactive jobs in function '__notice_path_change' with arguments 'VARIABLE SET PATH' called on line 1 of file /nix/store/r5brs3gn4amxbl1mrl4433inlghwl1r0-fish-3.2.2/share/fish/config.fish in event handler: handler for variable “PATH” called on line 198 of file /nix/store/r5brs3gn4amxbl1mrl4433inlghwl1r0-fish-3.2.2/share/fish/config.fish PATH has changed to /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Library/Apple/usr/bin:/usr/local/MacGPG2/bin:/Library/TeX/texbin:/Users/d12frosted/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/Users/d12frosted/.config/bin:/Users/d12frosted/.local/bin This is a login shell Job control: Only on interactive jobs in function '__notice_path_change' with arguments 'VARIABLE SET PATH' called on line 1 of file /nix/store/r5brs3gn4amxbl1mrl4433inlghwl1r0-fish-3.2.2/share/fish/config.fish in event handler: handler for variable “PATH” called on line 129 of file /nix/store/r5brs3gn4amxbl1mrl4433inlghwl1r0-fish-3.2.2/share/fish/config.fish Welcome to fish, the friendly interactive shell Type help for instructions on how to use fish [17:37:38] @MacBook-Pro ~ λ
So as you can see, the value of PATH
is almost correct before fish
is loaded. The value is in correct order, but lacks few entries. And then it gets the missing entries, but also gets reordered on line 198 of $__fish_data_dir/config.fish
.
Turns out, fish
mimics behaviour of path_helper
macOS (BSD) utility, which makes sure that entries from /etc/paths
file and all files under /etc/paths.d/
are present in the PATH
.
path_helper(8) Nixpkgs System Manager's Manual path_helper(8) NAME path_helper — helper for constructing PATH environment variable SYNOPSIS path_helper [-c | -s] DESCRIPTION The path_helper utility reads the contents of the files in the directories /etc/paths.d and /etc/manpaths.d and appends their contents to the PATH and MANPATH environment variables respec‐ tively. (The MANPATH environment variable will not be modified unless it is already set in the en‐ vironment.) Files in these directories should contain one path element per line. Prior to reading these directories, default PATH and MANPATH values are obtained from the files /etc/paths and /etc/manpaths respectively. Options: -c Generate C-shell commands on stdout. This is the default if SHELL ends with "csh". -s Generate Bourne shell commands on stdout. This is the default if SHELL does not end with "csh". NOTE The path_helper utility should not be invoked directly. It is intended only for use by the shell profile. Mac OS X March 15, 2007 Mac OS X
And this is how it's implemented in fish
(inside $__fish_data_dir/config.fish
):
# # Some things should only be done for login terminals # This used to be in etc/config.fish - keep it here to keep the semantics # if status --is-login if command -sq /usr/libexec/path_helper # Adapt construct_path from the macOS /usr/libexec/path_helper # executable for fish; see # https://opensource.apple.com/source/shell_cmds/shell_cmds-203/path_helper/path_helper.c.auto.html . function __fish_macos_set_env -d "set an environment variable like path_helper does (macOS only)" set -l result # Populate path according to config files for path_file in $argv[2] $argv[3]/* if [ -f $path_file ] while read -l entry if not contains -- $entry $result test -n "$entry" and set -a result $entry end end <$path_file end end # Merge in any existing path elements for existing_entry in $$argv[1] if not contains -- $existing_entry $result set -a result $existing_entry end end set -xg $argv[1] $result end __fish_macos_set_env PATH /etc/paths '/etc/paths.d' if [ -n "$MANPATH" ] __fish_macos_set_env MANPATH /etc/manpaths '/etc/manpaths.d' end functions -e __fish_macos_set_env end # ... end
In short, it constructs a list of entries from /etc/paths
file plus files from /etc/paths.d
and appends to the result all missing entries from PATH
variable. Since $HOME/.nix-profile/bin
is not in /etc/paths
, it is added to the end of the result.
I am not sure why this mechanism exists in the first place, I suspect that it's needed for building proper PATH
during system loading and for operation of macOS applications (which is a constant source of confusion). If anyone knows more, please share your knowledge via comments or email, I will include better explanations instead of my speculations.
While we learned the reason this value is incorrect, it's still unclear how and by whom PATH
is fixed when using bash
and how to fix it in fish
.
By quick inspection of contents of /run/current-system
and /run/current-system/etc
, I find an interesting file /run/current-system/etc/bashrc
.
λ la /run/current-system/ total 68K dr-xr-xr-x 15 root wheel 480 Jan 1 1970 . drwxrwxr-t 11741 root nixbld 367K May 20 09:14 .. lrwxr-xr-x 1 root wheel 76 Jan 1 1970 Applications -> /nix/store/4w1af25hb32hqd31sh7pwm4vd00dpzw2-system-applications/Applications dr-xr-xr-x 5 root wheel 160 Jan 1 1970 Library -r-xr-xr-x 1 root wheel 40K Jan 1 1970 activate -r-xr-xr-x 1 root wheel 6.9K Jan 1 1970 activate-user dr-xr-xr-x 2 root wheel 64 Jan 1 1970 darwin -r--r--r-- 1 root wheel 4.2K Jan 1 1970 darwin-changes -r--r--r-- 1 root wheel 38 Jan 1 1970 darwin-version lrwxr-xr-x 1 root wheel 51 Jan 1 1970 etc -> /nix/store/5069ikh9adm1m98fjxisgp6m7bn5jzwa-etc/etc lrwxr-xr-x 1 root wheel 59 Jan 1 1970 patches -> /nix/store/l4dwcgs0zqh5z6b2b4z1wax4fwamg5fg-patches/patches lrwxr-xr-x 1 root wheel 55 Jan 1 1970 sw -> /nix/store/jj97rcxh8z2fnn45bcd9xwm08xi3vdcy-system-path -r--r--r-- 1 root wheel 13 Jan 1 1970 system -r--r--r-- 1 root wheel 96 Jan 1 1970 systemConfig dr-xr-xr-x 3 root wheel 96 Jan 1 1970 user
λ la /run/current-system/etc/ total 0 dr-xr-xr-x 9 root wheel 288 Jan 1 1970 . dr-xr-xr-x 3 root wheel 96 Jan 1 1970 .. lrwxr-xr-x 1 root wheel 54 Jan 1 1970 bashrc -> /nix/store/b17sn0hfampy7fl1y0lf7nbckv2gfyvb-etc-bashrc dr-xr-xr-x 5 root wheel 160 Jan 1 1970 fish dr-xr-xr-x 4 root wheel 128 Jan 1 1970 nix lrwxr-xr-x 1 root wheel 54 Jan 1 1970 shells -> /nix/store/dyprd01kgm00asrnd7dv0rdmg1fk8855-etc-shells lrwxr-xr-x 1 root wheel 54 Jan 1 1970 skhdrc -> /nix/store/dd9hd30wlgbv4f2qfp1v863wm2wi8pkk-etc-skhdrc dr-xr-xr-x 3 root wheel 96 Jan 1 1970 ssh dr-xr-xr-x 3 root wheel 96 Jan 1 1970 ssl
# content of /run/current-system/etc/bashrc # /etc/bashrc: DO NOT EDIT -- this file has been generated automatically. # This file is read for interactive shells. [ -r "/etc/bashrc_$TERM_PROGRAM" ] && . "/etc/bashrc_$TERM_PROGRAM" # Only execute this file once per shell. if [ -n "$__ETC_BASHRC_SOURCED" -o -n "$NOSYSBASHRC" ]; then return; fi __ETC_BASHRC_SOURCED=1 # Don't execute this file when running in a pure nix-shell. if test -n "$IN_NIX_SHELL"; then return; fi if [ -z "$__NIX_DARWIN_SET_ENVIRONMENT_DONE" ]; then . /nix/store/arcg1b2dbhmhj31xnm2f4xxgfsrzpnph-set-environment fi # Return early if not running interactively, but after basic nix setup. [[ $- != *i* ]] && return # Make bash check its window size after a process completes shopt -s checkwinsize # Read system-wide modifications. if test -f /etc/bash.local; then source /etc/bash.local fi
As you can see, it sources /nix/store/arcg1b2dbhmhj31xnm2f4xxgfsrzpnph-set-environment
file, which basically makes sure that $HOME/.nix-profile/bin
is at the beginning of PATH
:
# content of /nix/store/arcg1b2dbhmhj31xnm2f4xxgfsrzpnph-set-environment # Prevent this file from being sourced by child shells. export __NIX_DARWIN_SET_ENVIRONMENT_DONE=1 export PATH=$HOME/.nix-profile/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin export EDITOR="nano" export NIX_PATH="ssh-auth-sock=/Users/d12frosted/.config/gnupg/S.gpg-agent.ssh:ssh-config-file=/Users/d12frosted/.config/.ssh/config" export NIX_SSL_CERT_FILE="/etc/ssl/certs/ca-certificates.crt" export PAGER="less -R" export XDG_CONFIG_DIRS="$HOME/.nix-profile/etc/xdg:/run/current-system/sw/etc/xdg:/nix/var/nix/profiles/default/etc/xdg" export XDG_DATA_DIRS="$HOME/.nix-profile/share:/run/current-system/sw/share:/nix/var/nix/profiles/default/share" # Extra initialisation # reset TERM with new TERMINFO available (if any) export TERM=$TERM export NIX_USER_PROFILE_DIR="/nix/var/nix/profiles/per-user/$USER" export NIX_PROFILES="/nix/var/nix/profiles/default /run/current-system/sw $HOME/.nix-profile" # Set up secure multi-user builds: non-root users build through the # Nix daemon. if [ ! -w /nix/var/nix/db ]; then export NIX_REMOTE=daemon fi ~
So it seems that nix-darwin
is fixing PATH
for bash
, but it doesn't fix PATH
for fish
. While the issue is not fixed in the upstream, it's easy to fix it locally by adding required values in programs.fish.shellInit
.
Since I didn't want to mess too much with specific values, instead, I simply remember the original value of PATH
before fish
reconstructed its path and then in my user init
code I fix the order like this:
programs = { fish.enable = true; fish.shellInit = '' __nixos_path_fix ''; }; # see https://github.com/LnL7/nix-darwin/issues/122 environment.etc."fish/nixos-env-preinit.fish".text = lib.mkMerge [ (lib.mkBefore '' set -g __nixos_path_original $PATH '') (lib.mkAfter '' function __nixos_path_fix -d "fix PATH value" set -l result (string replace '$HOME' "$HOME" $__nixos_path_original) for elt in $PATH if not contains -- $elt $result set -a result $elt end end set -g PATH $result end '') ];
Rebuild and enjoy coreutils
and alike!
Safe travels.