Systemd user units and /no-such-path

tl;dr How can I set the correct PATH for systemd user units before starting a session?

The problem

To bind mount some directories into my home directory I have the following systemd user unit:

[Unit]
Description=Bind mount /scratch/%u/%I
ConditionPathIsDirectory=/scratch/%u/%I
ConditionPathIsDirectory=%h/%I
ConditionPathIsMountPoint=!%h/%I

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/env bindfs --no-allow-other "/scratch/%u/%I" "%h/%I"
ExecStop=/usr/bin/env fusermount -u "%h/%I"

[Install]
WantedBy=default.target

This unit is stored in my home directory at ~/.config/systemd/user/bindfs@.service and it has to be stored there because I use this config on multiple machines, not all of which have NixOS installed.

However, this unit completely fails to run on NixOS with the error:

/usr/bin/env: ‘bindfs’: No such file or directory

This is due to the fact that on NixOS systemd starts out with PATH=/no-such-path, as introduced by this commit

https://github.com/NixOS/systemd/commit/bdfe641e4b713ce77d65c411fc589144d6f58849

Generally this makes sense for declarative service management but makes it entirely impossible to do any kind of old-fashioned stateful service management as is common in configurations that are used on different machines. This comes as kind of a surprise to me because this way it is not even possible to access programs installed via environment.systemPackages which should be available to all users (at least according to the docstring in system-path.nix).

Attempted workarounds

In my configuration.nix I have already set

{
  environment.extraInit = lib.mkAfter ''
    [ -f ~/.profile ] && . ~/.profile
    ${config.systemd.package}/bin/systemctl --user import-environment PATH
  '';
}

to import my local environment variables into the shell and the service manager for interactive logins (graphical or ssh). However, this snippet is only sourced when the login shell is started which happens after systemd starts the user session which means that the aforementioned user unit will not see the updated PATH. In fact, when I restart the failed unit manually after my shell prompt has loaded

$ systemctl --user restart bindfs@data.service

it succeeds because PATH has been updated. Now I could of course add this command to my .bashrc or whatever but this completely defeats the purpose of systemd user services.

So far I was unable to find a solution to this problem. It is also kind of a hen-and-egg problem because I’d like systemd to inherit PATH which can happen earliest in the shell init script but spawning the shell is done my the systemd user service manager (As you can see by typing systemctl status. Your login shell is nested under user-<UID>.slice).

Notes

As an aside, for a graphical login (at least via GDM) you don’t get a login shell and therefore none of the shell init files are sourced. To source your .profile and import the path into the service manager I added the same snippet as above to the sessionCommands:

{
  services.xserver.displayManager.sessionCommands = ''
    [ -e ~/.profile ] && . ~/.profile
    ${config.systemd.package}/bin/systemctl --user import-environment PATH
  '';
}

Just now I have found another workaround which at least gives me access to the programs installed via environment.systemPackages. It is possible to set the default PATH for user units within systemd. In principle this is a really bad idea because setting PATH this way would clobber whatever the system has set globally but since NixOS sets this to /no-such-path there is really nothing to lose.

{
  systemd.user.extraConfig = ''
    DefaultEnvironment="PATH=/run/current-system/sw/bin"
  '';
}

This still has the downside that I’m still not able to access the user environment. In principle I could just set PATH=/etc/profiles/per-user/henri/bin:/run/current-system/sw/bin but that is then hardcoded to my username. Actually this wouldn’t be a problem because I’m the only user on my laptop but it seems hackish and feels “unclean”.

I have not yet tried whether I can use the systemd replacement patterns, e.g. %u for the current user as in PATH=/etc/profiles/per-user/%u/bin:/run/current-system/sw/bin. Unfortunately, updating /etc/systemd/user.conf requires restarting the service manager which effectively means rebooting and so far I was too lazy to do that.

Here is the workaround I came up with:

  systemd.services."my-service-running-my-command" = {                                                                                                                                                                              
    script = ''                                                                                                                                                                                                                             
      # Note: This assumes the command is available for this user                                                                                                                                                     
      ${pkgs.fish}/bin/fish -l -c "my-command"                                                                                                                                                                   
    '';                                                                                                                                                                                                                                     
    serviceConfig = {                                                                                                                                                                                                                       
      Type = "oneshot";                                                                                                                                                                                                                     
      User = "<my-user-name>";                                                                                                                                                                                                                        
    };                                                                                                                                                                                                                                      
  };

This SO basically gave the answer: Make systemd service inherit environment variables from /etc/profile.d - Unix & Linux Stack Exchange
The trick is launch a (login) shell via systemd and then launch the actual command within that shell.