[Q/A] Can Flakes have teardown scripts?

I am seeing more and more configs using flakes and installing the likes of nix-darwin and home-manager as flakes. However, when uninstalling I am not seeing a feature to uninstall a flake properly e.g to revert some system changes. This makes me wonder if flakes should have push up and teardown features.
However, do they already have this?

You do not “install flakes”.

Flakes is just a container for a variety of other nix expressions.

You can have system configurations, darwin configs or home manager configs, corresponding modules, “dev shells”, or packages in this container.

Some of them can be “installed”, eg. by using nixos-rebuild or nix profile, others just be "entered" eg. shells. Others can be "run", like the apps`.

I do not see how or where tearX would be to be used.

I assume he’s referring to the installPhase of a derivation.

I’m not very familiar with derivations, but I don’t see any options in the docs to reverse the install phase.

This is pretty much impossible to implement in reality. If you want to go back to the state things were in previously, you have to make a backup and restore it.

I understand that you’re only talking about programs that do actually change your system. Anything installed or uninstalled with nix profile has no direct effect on the system, and uninstalling completely restores the previous state your environment was in. The whole idea of this part of the installation is that it has no side-effects. It replaces your old environment with a new one that contains more (or fewer) packages, but the old one is still there and you can return whenever you want.

When nix-darwin or home-manger make changes to your system, it does so by assuming control of a part of your system. Once it has done that, it easy to go back to the first state you were in. You’re in the same situation as with nix profile: the changes it makes are conceptually just different realities of what your system state can be, and returning to a previous one is possible by just swapping out exactly the state that nix profile controls with a previous version.
However, it is only possible for those files that home-manager or nix-darwin have control over. Any other files might have changed the last time you looked at them, and as there was no set of instructions written down somewhere that allows you to reproduce that state, it is gone.

So let’s just say we wanted to implement this feature, how would we do it?

  1. You could try to auto-generate a configuration from the current state of your user/system envrionment.
    This can actually work, and it’s precisely what nixos-generate-config does. However, this only works if you do it once and then move all of the system state into a declarative configuration. With home-manager and nix-darwin, this isn’t possible, as they exist alongside mutable state that can change at any time, so you never have a full screenshot of the system state before they were installed.
  2. You could make home-manager create a backup of every file that it overwrites, and add a note if it created a file where there previously was none. Then you’d roll back all those changes when you want to remove home-manager. But again, this is not a snapshot of a single point in time where the system worked. Doing this can catastrophically break the user’s system!
  3. You could make home-manager create a full system backup before the very first invocation and then restore that backup afterwards.

These would all work as features inside of home-manager and nix-darwin, and hey, maybe there’s some merit to that, though I would argue that making a system backup and restoring that is probably best left to the user to do, as they have to decide what mechanism to use anyway.

But as a general feature for flakes, they fundamentally would require nix profile remove or whatever mechanism you use to uninstall flakes to have side-effects.

Apart from being against the entire idea of Nix as a whole, practically speaking this would mean that doing a nix profile rollback could not reliably return you to the state the environment was in before. Every install or uninstall command now has the potential to do some irreversible action on the side, and that completely ruins all the reproducibility guarantees Nix offers.

1 Like

The installPhase only writes to the derivation’s output, right? So reversing it would mean to just remove the output of the derivation from the Nix Store.

3 Likes

I do not see how or where tearX would be to be used.

where flakes extend the functionality of Nix and create backup/modify system files or make symbolic links that are left broken after removing nix.

example between nix and nix-darwin
the following was left

/etc/ssl/certs/ca-certificates  --> -/etc/static/ssl/certs/ca-certificates.crt 

and because -/etc/static is link to the nix store it was left broken.

Because of curls terrible error messages and the fact macOS itself uses the keychain for certs
I was left not being able to complete nix build and so took me longer than normal to find the issue.

If I had installed nix-darwin and home-manager via channel I would have use uninstall scripts that come with each. Of cause , aren’t used for flake based installs.

I guess better install/uninstall scripts outside nix are a better option?

or perhaps install nix-darwin and home-manager via channels and have everything else in my setup as flakes?

However, it is only possible for those files that home-manager or nix-darwin have control over . Any other files might have changed the last time you looked at them, and as there was no set of instructions written down somewhere that allows you to reproduce that state, it is gone .

Yes this is am issue and it defeats the object of having flakes

This is a bug in nix darwin and not in “flakes”, you would have the same problem with a non flake nix-darwin configuration.

Ahhh I see, I think I understand your issue better now.

That depends, right? I assume you’re talking about the differences between the traditional standalone and the flake-based standalone installs?

Because I’m not sure how those differ in terms of internal setup, but in both cases, you call an install command to get started:

$ nix-shell '<home-manager>' -A install

vs

nix run home-manager/master -- init --switch

and I’m not sure why you couldn’t have an deinit output for nix run to use.
That just seems like a missing feature in home-manager, not a design flaw with flakes.

Yeah exactly. I think we saw this very clearly with the determinate installer as well. The normal nix installer is ok, but there’s just so many potential edge-cases that it didn’t handle properly, and maintaining a huge bash script like that is difficult. Uninstalling is a quite manual process, and there’s loads of issues with broken half-installed states that are tedious to fix. The install receipt of the determinate installer ensures that you know exactly what the installer changed and the uninstall script being stored right next to it makes uninstalling as easy as installing.

That being said, the surface area of what Nix changes on your system is tiny compared to what home-manger and nix-darwin can change on your system.

Not sure how much that would help. If the uninstall script of the regular installs has bugs, you can hit the same problems potentially, but it sure is the more proven method.

1 Like

Not sure how much that would help. If the uninstall script of the regular installs has bugs, you can hit the same problems potentially, but it sure is the more proven method.

I was thinking because you could use the uninstall scripts provided foreach in turn.

Thanks for the info, I think I will write some scripts .

(I’m about to pick on how difficult this goal is, so I’ll say first that I do want the ecosystem to aspire to a leave-no-trace ethic. Some fraction of people who have install/uninstall trouble and/or bounce off of Nix semi-fairly read failures here as a sign that Nix can’t deliver on its core promises.)

Another way to frame why this is hard (and how hard it is) is to underscore that this is basically the same minefield that tools like Chef and Ansible try to navigate.

In some fraction of these cases, the main Nix/NixOS affordances can’t help us. We depend on being able to generate a set of artifacts that correspond to a configuration and then atomically change between them without having to worry much about the previous state (beyond having a pointer to it that we could use to roll back).

Teardown scripts have to cope with an infinite set of always-changing system-idiomatic state/config and the higher stakes of destructive actions.

For example, nix-darwin doesn’t need to know what your current defaults settings are in order to build a script that will apply a set of declarative options as a one-way transform.

But when you’re asking about uninstalling/tearing down/reversing the nix-darwin install, what’s the right thing to do with these defaults?

  • You can leave them as-is. It’ll confuse/surprise some users (especially if the uninstall undoes some parts of what nix-darwin did but not others), but it won’t significantly change the appearance or behavior of their system.
  • You could try to restore them to their values before the first nix-darwin activation (which will also confuse/surprise some users), but:
    • You’d have to have recorded what all of those old values were.

    • Since these settings can also be mutated by OS tools and by nix-darwin at any point after the first activation, you can’t really assume that a pre-first-activation snapshot is canonical. The last manually-set value could pop up between any pair of activations.

    • If the first nix-darwin activation was a few macOS versions ago, some of these defaults may have been changed, and “restoring” the old value won’t actually restore the old functionality.

    • I’m not certain, but I think some settings aren’t explicit in defaults before you set/change them, so I guess it’d also either need to know which ones to delete or what the default system value is in order to restore something like the old behavior.

    • Since each iterative activation isn’t fully and deterministically reverting all changes to the defaults, the system’s active defaults may include things that were once set by a nix-darwin activation but which have since been removed from the user’s configs. Should they be left as-is, since they weren’t set before the first activation? Should they be deleted or re-set to defaults, even though this could be a surprising system state change to the user?

      This problem could be mitigated by making every activation use a more complete ~aspirationally-idempotent routine that attempts to unset the nix-darwin-controlled settings before setting the new ones, but the new build won’t have access to the old settings. I guess it might have to depend on having access to a teardown script from the previous generation. I think this would be ~right in some sense (and would at least help ensure the teardown script gets fixed when it breaks), but it also increases the stakes of an activation failure.

If we were to get serious about aspiring to this across the ecosystem, I’ve wondered if it would make sense to build reusable tooling for doing it consistently. Features might be like:

  • a DSL for directly declaring target filesystem states
  • tracks what mutations were performed to reach those states
  • tracks any time it encounters mutations made by the user or other tools between our own mutations
  • has an interface for reviewing what’s been changed and selectively undoing these so that users can get what they want/expect out of the uninstall process?

(Something like this, if it were a single binary, might also be able to do some dual-duty as a way for describing install/copy/patch steps in Nix packages in a way that requires fewer external tool invocations?)

2 Likes