Edit: Proposal on hold; see update.
system.stateVersion is widely acknowledged to be an imperfect thing. I’d like to propose incrementally augmenting it with a less imperfect thing.
Problems I want to address:
- Users, new and old, perennially want to update
system.stateVersion. We can tell them not to until we’re blue in the face, but there’s a certain kind of OCD computer toucher who simply cannot allow anything that looks like a date in their system config to be more than a year old; and the NixOS community has, I would wager, more than the base rate of such users. Some have suggested we mollify these users by makingstateVersionnot look like a date, but I think that’s letting the tail wag the dog. It’s not entirely unreasonable to want to migrate your system state periodically to conform to current best practices. The biggest problem with actually doing so is that: - The effects of
system.stateVersionare opaque. The number of modules that use it is in the dozens — not impossible to manually review, especially given that many of those modules you can know are not relevant to your system, but enough to make it possible for something to slip through if you are less than maximally careful. And even if you are careful: - Updating
system.stateVersionis all-or-nothing. You have to migrate the data for every module that uses it at once, which increases both the risk of getting it wrong and the cost of doing it at all. AFAIK, there are no intermodule dependencies usingsystem.stateVersion— no module assumes that another module will operate in a certain way because of the current value ofsystem.stateVersion— so there’s no reason for it to be this way, other than avoiding the clutter of a lot of individualstateVersiondefinitions.
I’d like to get some support for doing the following:
- Declare a new option
system.moduleStateVersionsof typeattrsOf int. Nothing should depend on this option, and it’ll only be really useful once we’ve done more work, but we can create it now. - On a per-module basis, for modules that use or want to start using
system.stateVersion, use the following pattern:- Declare a new
stateVersionoption for that module (services.whatever.stateVersion) of typeint. It should take a default value that depends onsystem.stateVersion. Nothing else in the module may depend onsystem.stateVersion. Everything you want to version depends on the module-localstateVersioninstead. (Edit: Original credit to @ElvishJerricco for seeding this idea in my brain a year ago, apparently.) - Inside the
config = lib.mkIf cfg.enableblock, setsystem.moduleStateVersions."services.whatever" = cfg.stateVersion;.
- Declare a new
- Put the above in the NixOS documentation.
Example module:
{ config, lib, pkgs, ... }:
let
cfg = config.services.whatever;
in
{
options.services.whatever = {
enable = lib.mkEnableOption "whatever";
# or: stateVersion = lib.mkStateVersionOption "the whatever service" config [ "23.11" "25.05" "26.05" ];
stateVersion = lib.mkOption {
description = ''
Versions the format of persistent state used by the whatever
service. Changing this value requires understanding the
module well enough to be able to migrate this state by hand.
'';
type = lib.types.int;
default = lib.lists.findFirstIndex (lib.versionOlder config.system.stateVersion) 3 [ "23.11" "25.05" "26.05" ];
defaultText = ''
Depends on system.stateVersion:
if >= 26.05: 3
if >= 25.05: 2
if >= 23.11: 1
otherwise: 0
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.whatever = {
# ...
something = if cfg.stateVersion >= 3 then "newest" else "legacy";
somethingElse = if cfg.stateVersion >= 1 then "modern-ish" else "really-old";
};
system.moduleStateVersions."services.whatever" = cfg.stateVersion;
};
}
Doing this incrementally immediately solves problem 3 for the modules that have been incrementally updated. By setting services.whatever.stateVersion themselves, users have a way to locally migrate modules one at a time instead of being forced into doing it all at once. Clutter is still avoided in the default case because these options take their default from system.stateVersion.
Doing this for all modules solves problems 1 and 2 as well, via inspecting the attributes of system.moduleStateVersions. Since modules only add attributes to this option when they are enabled, printing this config value will show only the modules that are relevant to the user. Comparing this config value before and after a bump of system.stateVersion will show which modules need migration, and then users can incrementally migrate their module state by fixing the services.whatever.stateVersion options to their old values until they get around to migrating that specific service.
None of this precludes doing anything more clever in the future, either with system.stateVersion or with automating migrations in module code. This isn’t meant to be the perfect solution for persistent state; I think several folks have been looking for that for a while, and if it exists, it’s not likely to be implemented soon. It’s just less imperfect, and it’s a small enough proposal to be tried on a few modules to find any rough edges before going on a treewide crusade.
If this meets with general approval, then based on the conversation in this thread, I’d suggest starting with jellyseerr: rename jellyseerr to seerr by nicdumz · Pull Request #500782 · NixOS/nixpkgs · GitHub, which very recently added a dependency on stateVersion to a module that previously didn’t use it. We can try this pattern out there and see how it works, and give the OP of that Discourse thread the opportunity to ‘preemptively’ migrate their config by setting services.seerr.stateVersion = 1;.