Why use `exec` when creating a custom script in the Nix store (and how does `shellHook` work exactly)?

Continuing the discussion from How to add local files into nginx derivation for nix-shell?:

Here’s the slightly modified shell.nix from the post linked above (with a couple more questions in tow):

{ pkgs ? import <nixpkgs> {} }:
with pkgs;

let
  nginx-with-config = pkgs.writeScriptBin "my_nginx" ''
  # VVVV
    exec ${pkgs.nginx}/bin/nginx -c ${./nginx.conf} "$@"
  # ^^^^
  '';

in mkShell {
  name = "my-shell";
  buildInputs = [
    nginx-with-config 
  ];

  shellHook = ''
    my_nginx
    trap "my_nginx -s quit" EXIT
  '';
}

1. Why use exec?

The Nix expression works even after deleting exec. Are there any benefits?

2. Are commands in a shellHook executed in the same shell created by the Nix expression?

When exec is invoked with a command, “it replaces the shell without creating a new process”. For example, running

exec watch -n 1 "ls -al"

would replace the current shell, and CTRL+C would simply close the terminal window (e.g., in tmux).

Now, running the Nix expression above with nix-shell will drop into an interactive shell where nginx is already running, instead of my_nginx “consuming” the current shell. trap wouldn’t even have a chance to run, and yet it does because nginx will get shut down upon exit from the Nix shell. nginx will fork itself in the background (is that the right way to say it?) so this may changes things, and in this case my ignorance is more about Linux than Nix itself.

I thought that this is a sign that each command is being run in a new shell, but this may not be the case because the trap command works too, and if that would run in its own shell, then it would trap it’s own exit, and nginx would be stopped right after starting up. the nginx executable will fork itself in the background (not sure what the right way to say this).

Anyway, currently I see that shellHook working only if these commands are simply dropped into the newly generated Nix shell, because the alternative (where each commands will run in its own shell) doesn’t make sense,

bash -c 'my_nginx"'; bash -c 'trap "my_nginx -s quit" EXIT'

because the trap would simply detect the exit of its own shell (and shut down the newly started nginx instance right away).

Avoiding a new process is the benefit.

Yep. Configuring that shell is the point of the hook. https://github.com/NixOS/nix/blob/8d871e18225d39a4c256b5416cc275137b8769b9/src/nix-build/nix-build.cc#L511-L573

3 Likes

my_nginx is a separate script, and therefore runs in a subprocess. trap will behave as you expect it to (never execute) if you run nginx like so:

shellHook = ''
    exec ${pkgs.nginx}/bin/nginx -c ${./nginx.conf} "$@"
    trap "my_nginx -s quit" EXIT
  '';

To be even more clear, you can kind of read your current script as:

shellHook = ''
    bash /nix/store/<hash>-my_nginx # This spawns a new process, which gets immediately eaten by `exec nginx`. nginx forks *another* process and closes itself (but not the second nginx process, which "goes to the background"), allowing its parent to continue 
    trap "my_nginx -s quit" EXIT # This then runs because the original (=parent) process is still there, and in fact your interactive shell
  '';
2 Likes