nix-darwin icon indicating copy to clipboard operation
nix-darwin copied to clipboard

Support arbitrary keys in `system.activationScripts`

Open quentinmit opened this issue 1 year ago • 11 comments

This changes nix-darwin's behavior to match NixOS's handling of this option. Each key can specify a list of dependencies to control ordering, and every element of system.activationScripts will be used.

This also introduces system.userActivationScripts, again mirroring NixOS, and moves the user-level keys from system.activationScripts.

Care is taken to continue handling legacy keys such as system.activationScripts.{preActivation,preUserActivation,postActivation,...} with the same behavior they have already had.

Previously, the org.nixos.activate-system launchd daemon only ran the etc and keyboard activation scripts. This change adds a new onlyOnRebuild setting to existing keys to preserve that behavior. Newly defined keys will run at both rebuild and boot time, matching NixOS's behavior.

Fixes #663.

quentinmit avatar May 18 '23 00:05 quentinmit

I had a failing install-flake check so I dug deep into the logs.

First, it's complaining that /etc/nix/nix.conf already exists, but that seems to be normal based on looking at other recent PR's check logs. This is probably worth fixing as it means the CI doesn't actually test replacing nix.conf.

The nix-daemon activation script then runs, which checks if /etc/nix/nix.conf and /run/current-system/etc/nix/nix.conf are different, and triggers a SIGHUP. This was very confusing to me, until I realized that at the point the activation scripts run, /run/current-system is actually the old system.

It then waits for nix-daemon to start, but I'm guessing that what's happening is that nix-daemon hasn't actually shut down yet, so this check is a noop. Then in the next step, the nix-daemon isn't running (error: cannot connect to socket at '/nix/var/nix/daemon-socket/socket': Connection refused).

I'm guessing the nix-daemon activation script is just plain broken, and the fact that there are fewer activation scripts that run after it now is tickling a race condition that was previously dormant.

I reworked the code to check launchctl kickstart -p before the SIGHUP, which prints the pid of the running process. Then we can wait until the pid has changed. There's also launchctl kickstart -k that supposedly will kill the process and then restart it, but I can't tell what signal it will send, and presumably it doesn't use SIGHUP.

quentinmit avatar May 18 '23 23:05 quentinmit

I was looking for the same feature and was surprised that someone already supported it :sweat_smile:

Your solution works for my use-case

rvem avatar May 22 '23 06:05 rvem

Currently I've just worked around this by lib.mkBefore/lib.mkAftering existing activation scripts in my modules, which seems strictly worse. Unfortunately I don't think there's any way to avoid trusting external modules with the current architecture, and if they break something I doubt the interface used to poke at the activation scripts would be the deciding factor in whether users blame nix-darwin for it.

emilazy avatar May 22 '23 19:05 emilazy

In general I tried to keep compatibility with existing modules, however I intentionally excluded activation scripts from the public api for external modules. It would be nice if the module system provided better functionality for this but everything is global and thus accessible in all modules.

Allowing any imported code to do arbitrary things as root already doesn't seem like the best idea in the first place and while initially implementing the darwin modules I clearly noticed that almost none of the existing activation logic from NixOS could be reused as it's written with the assumption that it has full control over the entire system. Unlike with NixOS there's no way to rollback if activation breaks something for the host system.

I believe this PR preserves full compatibility with existing modules; users can still set exactly the same activation script keys and they will have exactly the same functionality, and they can also do the NixOS thing and define new activation script keys.

I agree it's a potentially dangerous interface, but you're already exposing the interface with preActivation/postActivation - this PR just makes it less painful to use from modules.

(BTW, the motivation for this is that I was starting to work on #618 and I found that there wasn't an easy way to add the required activationScript for it.)

quentinmit avatar May 22 '23 21:05 quentinmit

Currently I've just worked around this by lib.mkBefore/lib.mkAftering existing activation scripts in my modules, which seems strictly worse. Unfortunately I don't think there's any way to avoid trusting external modules with the current architecture.

I mostly agree with that, since everything is global there is indeed no way to actually prevent this. However currently at least insures that the included module was actually written with darwin in mind. The current changes would allow the following.

{
  imports = [ <nixpkgs/nixos/missing-functionality.nix> ];
  config = {
    something.enable = true;
  };
}

I would at least like this case like this to fail, one option could be to simply rename the option for activation snippets. This will still catch unintended use while enabling dynamic snippets and ordering both internally and for modules written for darwin specifically.

LnL7 avatar May 22 '23 21:05 LnL7

Seems reasonable; how about something like system.darwin.activationScripts or darwin.activationScripts (or even darwin.system.activationScripts if we expect to need to mirror other parts of the options hierarchy for Darwin-specific things)?

P.S. I wish everything wasn't global. I wish we had proper static checks. :(

emilazy avatar May 22 '23 21:05 emilazy

(Home Manager uses home.activation FWIW; the other existing system setting with a NixOS overlap that seems like it could be dangerous is stateVersion so maybe that should be darwin.stateVersion too?)

emilazy avatar May 22 '23 22:05 emilazy

I mostly agree with that, since everything is global there is indeed no way to actually prevent this. However currently at least insures that the included module was actually written with darwin in mind. The current changes would allow the following.

{
  imports = [ <nixpkgs/nixos/missing-functionality.nix> ];
  config = {
    something.enable = true;
  };
}

I would at least like this case like this to fail, one option could be to simply rename the option for activation snippets. This will still catch unintended use while enabling dynamic snippets and ordering both internally and for modules written for darwin specifically.

If you're okay with renaming it, we could name it system.darwinActivationScripts. I was trying to preserve compatibility with existing modules, which already use the name system.activationScripts{.preActivation,.postActivation,...}. I think user activation scripts can be left at system.userActivationScripts since those are less likely to break badly even if they're not darwin-specific.

quentinmit avatar May 23 '23 07:05 quentinmit

To clarify I'm ok with the changes if they don't remain under the same option as NixOS. For preActivation/postActivation we can use mkRenamedOptionModule to provide compatibility. I don't have a strong opinion on the new name, but maybe darwin.* is indeed a nice option instead of trying to keep it under the system.*.

LnL7 avatar Jun 20 '23 20:06 LnL7

To clarify I'm ok with the changes if they don't remain under the same option as NixOS. For preActivation/postActivation we can use mkRenamedOptionModule to provide compatibility. I don't have a strong opinion on the new name, but maybe darwin.* is indeed a nice option instead of trying to keep it under the system.*.

I don't believe mkRenamedOptionModule can be used since these are keys within an attrset.

quentinmit avatar Jul 19 '23 21:07 quentinmit

Yeah, could be that the implementation is a bit too simple to cover this case. But the same idea can be implemented using conditions and the warnings module.

LnL7 avatar Jul 23 '23 09:07 LnL7