Using hashes for `stateVersion` instead of human-readable strings

Lots of people change stateVersion believing it to be a version selector. It’s somewhat understandable, since it will match the version of NixOS that is deployed on first installation - bumping it when updating (especially when grepping for version numbers) is quite tempting. There’s a big ol’ comment in the generated default config about this, but that clearly doesn’t deter people.

Could we instead generate a random hash for each version and match against that, so that it doesn’t look so much like the version in use and users cannot change it without reading and understanding documentation?

13 Likes

I mean if we’re going to be radically redesigning stateVersion, let’s do it proper. Don’t use a single global stateVersion variable. Each component that needs stateVersion maintains its own setting for that, and the setting is just a freakin number, not a NixOS release.

services.postgresql.stateVersion = 6;
30 Likes

Could we maybe do a better job explaining what it actually does?

I was really confused when during one upgrade a module complained that I had to raise it for the new release and I thought I was supposed to not touch it.

In the end it turned to be way less scary than the comment makes it sound.
If I understood it correctly it just prevents package upgrades (mainly DBs) to newer versions in case you haven’t pinned them to a specific version.
Maybe it gates breaking path changes as well?

1 Like

It does whatever any individual module wants it to do. Any module can use it to determine what sort of state it should expect at runtime on the user’s system, and make configuration decisions based on that. Yea, sometimes it’s just about updating database packages. But it can be arbitrarily complex. A module could use it to anticipate the layout of a directory tree. Another module could use it to determine whether it needs to run a compatibility daemon for some purpose. It might determine what user to run a service as.

There is no singular purpose to stateVersion. That’s why I said it would be good if each module that needs it had its own stateVersion instead of having this scary global one.

This is why it’s written to sound scary. Because stateVersion can do anything. You came away from your experience thinking it just upgrades DB packages or something. But that’s not remotely true, and if you upgrade it in a way incompatible with your state and choice of modules, it could break things in incredibly confusing ways.

11 Likes

Thank you.
I agree that would be better and make it more obvious what it actually does.

Well, the problem is precisely that there is no obvious thing it does. If it’s going to be usefully explainable it has to be module-specific.

My idea was primarily trying to get some improvement through a bit of a path-of-least-resistance measure. It’d be really nice to remove yet another footgun.

I have to say, adding a confusing new option to every module as effectively in-config state also feels wrong to me. It’s also used here and there to prevent behavior changes between NixOS versions, it’d be very silly to have to set like 10 of these to use Gnome+X11+pipewire just because we might decide to remove some kind of default xorg package in the future.

1 Like

the problem with hashes is that they’re not easily comparable for before/after, but perhaps a ULID or UUIDv7 could work for being opaque and time-sortable.

It might even have the benefit that it can be bumped every time there’s a state-migration breaking change, not just at releases

4 Likes

Some guidelines like “only one stateVersion per module” or similar might be necessary.

The advantage would be that users could more easily search and understand it.
When the release notes mention “breaking change behind xorg.stateVersion > 7” a user can search for that and sees that it is related to his xorg config.

The docs for stateVersion currently just tell me to carefully check but don’t tell a user how to do so.
Maybe there is a convenient nix eval thing other than just grepping through the nixpkgs repo.

Do not change this value unless you have manually inspected all the changes it would make to your configuration, and migrated your data accordingly.

Btw. why are there default packages and data paths set at all?
Wouldn’t it be better to not provide a default and let the user choose right from the beginning?
It is an honest question I try to understand the problem better.
The recommended defaults could still be mentioned as an example.
Probably wouldn’t work for everything but for a lot of things this could work.

1 Like

That would not help, because any module you pull in might have one of these, and e.g. GNOME pulls in a lot of them. I’d not even suggested some modules might have more than one.

But yeah, as long as it’s not an overwhelming amount doing this per-module makes way more sense.

Because services.xserver.enable = true; is much easier than writing a few hundred lines of code, all while figuring out all the tiny components that make up a modern Xorg deployment in the first place.

There has to be an abstraction layer somewhere, and there needs to be a way to make changes to that in a way that is transparent to users. A global stateVersion is unfortunately a pretty bad way to do it, though.

4 Likes

A module-specific stateVersion would be an improvement over a global one. It’d allow setting a minimum within one module without having to worry about other modules, and it also means that it’s much easier to check what needs migration between versions, rather than “read all the release notes, gl” before bumping just one version.

But regarding not-setting a default, that may be beneficial in the case of psql, I don’t think it’s a tall order to ask that people directly set the psql package version compared to implicitly setting it based on a state version. IMO psql doesn’t need a state version. Or to put it more plainly, an abstraction that doesn’t actually save effort and causes more confusion is a worthless abstraction.

(The migration path would include a warning on not-explicitly set psql package for at least a release, ofc.)

6 Likes

But it gives a wrong impression that this is all there is to it.
Then we surprise the user at some point with the rest, better to show them upfront what is required.

I agree and for a lot services just defining the package and maybe a path could be enough.
More complex services might require a more complex solution.

How would a module-specific stateVersion work?
Would a new install include a massive wall of stateVersions in configuration.nix for every module that includes one?

I was under the impression that the usefulness of a stateVersion was that it allowed changing defaults in a backwards-incompatible way.

I don’t think it would be a good user experience to have to look up the current stateVersion of every module before you start using it, otherwise be stuck on the oldest stateVersion.

I suppose modules could error with the latest value if it’s not provided, but that somewhat breaks the magic of services.foo.enable = true; if you have to enable it, attempt to rebuild, then fix it to include the stateVersion.

4 Likes

You already do have to do that today, just implicitly, because the stateVersion is auto-set at install and you don’t know what modules currently or will use it.

Obviously the UX is bad, state versions should be used very sparingly and just generally be discouraged. I made a case above that it could even be removed from some modules, if all it’s doing is controlling a package version.

IMO it should only be used if there’s a lot of boilerplate to save when switching between versions of a pacakge or config file format or so on, AND that requires managing external state that nix cannot help with. Masking the value of a single option is not saving much boilerplate.

Ideally we could also name it something more self-explanatory.

2 Likes

well… since you started playing this game i guess i will join in too, it can be fun :blush:

  • imagine services (modules) which aren’t core to nixos (backup service, for example) weren’t shipped directly with nixpkgs but in external repos
  • imagine that services (modules) would adhere to some form of contract so they weren’t so tightly coupled
  • imagine these external services (modules) would use sematic versioning and we had a reasonable way to resolve this and store in lock files

then it looks like we have an os we can independently update services and feel confident about advanced workflows… sounds nice

this community is really cool and i see a bunch of pieces that could fit really well together - keep up the great work everyone :sunglasses:

3 Likes

That is an interesting future to look to, but I don’t think this is at odds with it - a module-specific stateVersion still makes sense in that world, and if it’s going to exist we should standardize it, and clarify when it should and should not be used.

I’m with you, this is a nice brainstorming thread, I actually have high hopes that we can take this to something concrete. But let’s maybe keep the scope to the problem that can be solved in months rather than decades :wink:

This is a great idea. Also makes it possible to do multiple breaking changes on unstable.

Currently you’re limited to only making a single breaking change per 6 months per module

1 Like

yes, you are right about the state version coming into play on my scenario too - but as @ElvishJerricco mentioned it is more useful at the module level

don’t sell this community short! such great innovation happening lately :star_struck:

I thought the same thing but I figured a warning would suffice (as we do for renamed options) and use the latest as default? Then it does not break building but you can fix it.

2 Likes

The enable option itself could take int|null for the state version instead of bool.

3 Likes

In nix-darwin, system.stateVersion is an integer that we bump whenever a new conditional is added. We are happy with that and I would recommend the NixOS project adopt something similar. It would be a simple and clear improvement over the status quo without having to discuss any broader change. (Though we should ensure we don’t assign the same integers to the same option name…)

4 Likes