lib/deprecations: support deferred state-version warnings
Extend mkStateVersionOptionDefault so merged attrset callers can defer warning emission to config.warnings while still exposing the effective state-version default and option priority metadata. Add dedicated lib tests for deferred warning behavior on both legacy and current state versions. Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
@@ -109,11 +109,25 @@
|
||||
pathStr: attrs: migrate pathStr attrs;
|
||||
|
||||
/*
|
||||
Builds `default` and `defaultText` values for options whose defaults
|
||||
change based on `home.stateVersion`, while warning users on the legacy
|
||||
branch.
|
||||
Builds the state-version migration values for options whose defaults change
|
||||
based on `home.stateVersion`.
|
||||
|
||||
Example:
|
||||
In the default mode, this returns `default` and `defaultText` for direct use
|
||||
in `mkOption`, and emits the migration warning with `lib.warn` when the
|
||||
legacy branch is active.
|
||||
|
||||
In deferred mode (`deferWarningToConfig = true`), this keeps `default`
|
||||
pinned to `current.value` and additionally returns:
|
||||
- `warning`: the warning text to add to `config.warnings`
|
||||
- `shouldWarn`: whether the warning should be emitted
|
||||
- `effectiveDefault`: the state-version-selected value
|
||||
- `optionUsesDefaultPriority`: whether the option is still at its module default priority
|
||||
|
||||
Deferred mode is intended for merged attrset options where warning from the
|
||||
option default itself is too early to be silenced reliably by explicit user
|
||||
assignments. It requires passing both `config` and `options`.
|
||||
|
||||
Direct example:
|
||||
let
|
||||
stateVersionDefault = lib.hm.deprecations.mkStateVersionOptionDefault {
|
||||
inherit (config.home) stateVersion;
|
||||
@@ -126,6 +140,26 @@
|
||||
lib.mkOption {
|
||||
inherit (stateVersionDefault) default defaultText;
|
||||
};
|
||||
|
||||
Deferred-warning example:
|
||||
let
|
||||
stateVersionDefault = lib.hm.deprecations.mkStateVersionOptionDefault {
|
||||
inherit (config.home) stateVersion;
|
||||
inherit config options;
|
||||
since = "26.05";
|
||||
optionPath = [ "programs" "example" "settings" ];
|
||||
legacy.value = { FOO = "legacy"; };
|
||||
current.value = { };
|
||||
deferWarningToConfig = true;
|
||||
};
|
||||
in {
|
||||
options.programs.example.settings = lib.mkOption {
|
||||
default = { };
|
||||
inherit (stateVersionDefault) defaultText;
|
||||
};
|
||||
|
||||
config.warnings = lib.optional stateVersionDefault.shouldWarn stateVersionDefault.warning;
|
||||
};
|
||||
*/
|
||||
mkStateVersionOptionDefault =
|
||||
{
|
||||
@@ -135,6 +169,11 @@
|
||||
legacy,
|
||||
current,
|
||||
extraWarning ? "",
|
||||
config ? null,
|
||||
options ? null,
|
||||
warningPriority ? (lib.mkOptionDefault { }).priority,
|
||||
shouldWarn ? null,
|
||||
deferWarningToConfig ? false,
|
||||
}:
|
||||
let
|
||||
option = lib.showOption optionPath;
|
||||
@@ -149,12 +188,32 @@
|
||||
${option} = ${currentText};
|
||||
''
|
||||
+ lib.optionalString (extraWarning != "") ("\n" + extraWarning);
|
||||
canDeferWarning = config != null && options != null;
|
||||
optionInfo = lib.optionalAttrs canDeferWarning (lib.getAttrFromPath optionPath options);
|
||||
optionUsesDefaultPriority = canDeferWarning && optionInfo.highestPrio >= warningPriority;
|
||||
|
||||
usingLegacyBranch = lib.versionOlder stateVersion since;
|
||||
in
|
||||
assert lib.assertMsg (!deferWarningToConfig || canDeferWarning) ''
|
||||
`lib.hm.deprecations.mkStateVersionOptionDefault` requires both `config` and `options`
|
||||
when `deferWarningToConfig = true`.
|
||||
'';
|
||||
{
|
||||
default =
|
||||
if lib.versionAtLeast stateVersion since then current.value else lib.warn warning legacy.value;
|
||||
if usingLegacyBranch && !deferWarningToConfig then lib.warn warning legacy.value else current.value;
|
||||
defaultText = lib.literalExpression ''
|
||||
if lib.versionAtLeast config.home.stateVersion "${since}" then ${currentText} else ${legacyText}
|
||||
'';
|
||||
effectiveDefault = if usingLegacyBranch then legacy.value else current.value;
|
||||
inherit warning optionUsesDefaultPriority;
|
||||
shouldWarn =
|
||||
deferWarningToConfig
|
||||
&& usingLegacyBranch
|
||||
&& (
|
||||
if lib.isFunction shouldWarn then
|
||||
shouldWarn { inherit optionInfo optionUsesDefaultPriority; }
|
||||
else
|
||||
optionUsesDefaultPriority
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
|
||||
lib-types-gvariant-merge = ./gvariant-merge.nix;
|
||||
lib-types-state-version-option-default = ./state-version-option-default.nix;
|
||||
lib-types-state-version-option-default-deferred = ./state-version-option-default-deferred.nix;
|
||||
lib-types-state-version-option-default-deferred-current = ./state-version-option-default-deferred-current.nix;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
attrSetOfString = lib.types.attrsOf lib.types.str;
|
||||
currentDefault = lib.hm.deprecations.mkStateVersionOptionDefault {
|
||||
inherit (config.home) stateVersion;
|
||||
inherit config options;
|
||||
since = "26.05";
|
||||
optionPath = [
|
||||
"test"
|
||||
"values"
|
||||
"current"
|
||||
];
|
||||
legacy.value = {
|
||||
FOO = "legacy";
|
||||
};
|
||||
current.value = { };
|
||||
deferWarningToConfig = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.test.values.current = lib.mkOption {
|
||||
type = attrSetOfString;
|
||||
default = { };
|
||||
inherit (currentDefault) defaultText;
|
||||
};
|
||||
|
||||
config = {
|
||||
home.stateVersion = "26.05";
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = currentDefault.default == { };
|
||||
message = "Deferred mkStateVersionOptionDefault should keep the raw option default at current.value on current state versions.";
|
||||
}
|
||||
{
|
||||
assertion = currentDefault.effectiveDefault == { };
|
||||
message = "Deferred mkStateVersionOptionDefault should expose current.value as the effective default on current state versions.";
|
||||
}
|
||||
{
|
||||
assertion = currentDefault.optionUsesDefaultPriority;
|
||||
message = "Deferred mkStateVersionOptionDefault should still detect default priority on current state versions.";
|
||||
}
|
||||
{
|
||||
assertion = !currentDefault.shouldWarn;
|
||||
message = "Deferred mkStateVersionOptionDefault should not warn on current state versions.";
|
||||
}
|
||||
];
|
||||
|
||||
test.asserts.warnings.expected = [ ];
|
||||
|
||||
home.file."result.txt".text = ''
|
||||
currentFoo=${currentDefault.effectiveDefault.FOO or ""}
|
||||
'';
|
||||
|
||||
nmt.script = ''
|
||||
assertFileContent home-files/result.txt ${pkgs.writeText "state-version-option-default-deferred-current.txt" ''
|
||||
currentFoo=
|
||||
''}
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
attrSetOfString = lib.types.attrsOf lib.types.str;
|
||||
mkDeferredDefault =
|
||||
name: extraArgs:
|
||||
lib.hm.deprecations.mkStateVersionOptionDefault (
|
||||
{
|
||||
inherit (config.home) stateVersion;
|
||||
inherit config options;
|
||||
since = "26.05";
|
||||
optionPath = [
|
||||
"test"
|
||||
"values"
|
||||
name
|
||||
];
|
||||
legacy.value = {
|
||||
FOO = "legacy";
|
||||
};
|
||||
current.value = { };
|
||||
deferWarningToConfig = true;
|
||||
}
|
||||
// extraArgs
|
||||
);
|
||||
|
||||
effectiveValue =
|
||||
stateVersionDefault: value:
|
||||
lib.optionalAttrs stateVersionDefault.shouldWarn stateVersionDefault.effectiveDefault // value;
|
||||
|
||||
cases = {
|
||||
unset = {
|
||||
default = mkDeferredDefault "unset" { };
|
||||
expectedWarn = true;
|
||||
expectedPriority = true;
|
||||
};
|
||||
explicitEmpty = {
|
||||
default = mkDeferredDefault "explicitEmpty" { };
|
||||
expectedWarn = false;
|
||||
expectedPriority = false;
|
||||
};
|
||||
explicitLegacy = {
|
||||
default = mkDeferredDefault "explicitLegacy" { };
|
||||
expectedWarn = false;
|
||||
expectedPriority = false;
|
||||
};
|
||||
priorityOnlyExplicit = {
|
||||
default = mkDeferredDefault "priorityOnlyExplicit" { };
|
||||
expectedWarn = false;
|
||||
expectedPriority = false;
|
||||
};
|
||||
partial = {
|
||||
default = mkDeferredDefault "partial" {
|
||||
shouldWarn =
|
||||
{ optionUsesDefaultPriority, ... }:
|
||||
optionUsesDefaultPriority
|
||||
|| (config.test.values.partial != { } && !(config.test.values.partial ? FOO));
|
||||
};
|
||||
expectedWarn = true;
|
||||
expectedPriority = false;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options.test.values = lib.mapAttrs (
|
||||
name: case:
|
||||
lib.mkOption {
|
||||
type = attrSetOfString;
|
||||
default = { };
|
||||
inherit (case.default) defaultText;
|
||||
}
|
||||
) cases;
|
||||
|
||||
config = {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cases.unset.default.defaultText._type == "literalExpression";
|
||||
message = "Deferred mkStateVersionOptionDefault should return a literalExpression defaultText.";
|
||||
}
|
||||
{
|
||||
assertion = cases.unset.default.default == { };
|
||||
message = "Deferred mkStateVersionOptionDefault should keep the raw option default at current.value.";
|
||||
}
|
||||
{
|
||||
assertion = cases.unset.default.effectiveDefault == { FOO = "legacy"; };
|
||||
message = "Deferred mkStateVersionOptionDefault should expose the legacy effective default on old state versions.";
|
||||
}
|
||||
]
|
||||
++ lib.flatten (
|
||||
lib.mapAttrsToList (name: case: [
|
||||
{
|
||||
assertion = case.default.shouldWarn == case.expectedWarn;
|
||||
message = "Deferred warning logic failed for ${name}: expected shouldWarn=${builtins.toJSON case.expectedWarn} but got ${builtins.toJSON case.default.shouldWarn}";
|
||||
}
|
||||
{
|
||||
assertion = case.default.optionUsesDefaultPriority == case.expectedPriority;
|
||||
message = "Deferred priority detection failed for ${name}: expected optionUsesDefaultPriority=${builtins.toJSON case.expectedPriority} but got ${builtins.toJSON case.default.optionUsesDefaultPriority}";
|
||||
}
|
||||
]) cases
|
||||
);
|
||||
|
||||
test = {
|
||||
values = {
|
||||
explicitEmpty = { };
|
||||
explicitLegacy = {
|
||||
FOO = "legacy";
|
||||
};
|
||||
priorityOnlyExplicit.BAR = "explicit";
|
||||
partial.BAR = "partial";
|
||||
};
|
||||
|
||||
asserts.warnings.expected = lib.flatten (
|
||||
lib.mapAttrsToList (name: case: lib.optional case.expectedWarn case.default.warning) cases
|
||||
);
|
||||
};
|
||||
|
||||
warnings = lib.flatten (
|
||||
lib.mapAttrsToList (name: case: lib.optional case.default.shouldWarn case.default.warning) cases
|
||||
);
|
||||
|
||||
home.file."result.txt".text = ''
|
||||
unsetFoo=${(effectiveValue cases.unset.default config.test.values.unset).FOO or ""}
|
||||
explicitEmptyFoo=${
|
||||
(effectiveValue cases.explicitEmpty.default config.test.values.explicitEmpty).FOO or ""
|
||||
}
|
||||
explicitLegacyFoo=${
|
||||
(effectiveValue cases.explicitLegacy.default config.test.values.explicitLegacy).FOO or ""
|
||||
}
|
||||
priorityOnlyExplicitFoo=${
|
||||
(effectiveValue cases.priorityOnlyExplicit.default config.test.values.priorityOnlyExplicit).FOO
|
||||
or ""
|
||||
}
|
||||
priorityOnlyExplicitBar=${
|
||||
(effectiveValue cases.priorityOnlyExplicit.default config.test.values.priorityOnlyExplicit).BAR
|
||||
or ""
|
||||
}
|
||||
partialFoo=${(effectiveValue cases.partial.default config.test.values.partial).FOO or ""}
|
||||
partialBar=${(effectiveValue cases.partial.default config.test.values.partial).BAR or ""}
|
||||
'';
|
||||
|
||||
nmt.script = ''
|
||||
assertFileContent home-files/result.txt ${pkgs.writeText "state-version-option-default-deferred.txt" ''
|
||||
unsetFoo=legacy
|
||||
explicitEmptyFoo=
|
||||
explicitLegacyFoo=legacy
|
||||
priorityOnlyExplicitFoo=
|
||||
priorityOnlyExplicitBar=explicit
|
||||
partialFoo=legacy
|
||||
partialBar=partial
|
||||
''}
|
||||
'';
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user