357 lines
12 KiB
Nix
357 lines
12 KiB
Nix
{ lib }:
|
|
{
|
|
/*
|
|
Builds a standard warning for an option value shape that is deprecated.
|
|
|
|
Example:
|
|
mkDeprecatedOptionValueWarning {
|
|
option = [ "programs" "example" "settings" ];
|
|
old = "a list";
|
|
replacement = "`programs.example.settings.items`";
|
|
}
|
|
|
|
=> Using `programs.example.settings` as a list is deprecated and will be
|
|
removed in a future release. Please use `programs.example.settings.items`
|
|
instead.
|
|
*/
|
|
mkDeprecatedOptionValueWarning =
|
|
{
|
|
option,
|
|
old,
|
|
replacement,
|
|
details ? "",
|
|
}:
|
|
''
|
|
Using `${lib.showOption option}` as ${old} is deprecated and will be
|
|
removed in a future release. Please use ${replacement} instead.
|
|
''
|
|
+ lib.optionalString (details != "") ''
|
|
|
|
${details}
|
|
'';
|
|
|
|
# Builds a standard warning for an option value that has been renamed.
|
|
mkDeprecatedOptionValueRenameWarning =
|
|
{
|
|
option,
|
|
old,
|
|
replacement,
|
|
}:
|
|
''
|
|
The value ${old} for `${lib.showOption option}` is deprecated and will be
|
|
removed in a future release. Please use ${replacement} instead.
|
|
'';
|
|
|
|
/*
|
|
Returns a function that maps
|
|
[
|
|
"someOption"
|
|
["fooBar" "someSubOption"]
|
|
{ old = "someOtherOption"; new = ["foo_bar" "some_other_option"]}
|
|
]
|
|
|
|
to
|
|
[
|
|
(lib.mkRenamedOptionModule
|
|
(oldPath ++ ["someOption"])
|
|
(newPath ++ ["some_option"])
|
|
)
|
|
(lib.mkRenamedOptionModule
|
|
(oldPath ++ ["fooBar" "someSubOption"])
|
|
(newPath ++ ["foo_bar" "some_sub_option"])
|
|
)
|
|
(lib.mkRenamedOptionModule
|
|
(oldPath ++ ["someOtherOption"])
|
|
(newPath ++ ["foo_bar" "some_other_option"])
|
|
)
|
|
]
|
|
|
|
The transform parameter is a function that takes a string and returns a string.
|
|
It is applied to each element of the old option path to generate the new option path.
|
|
Defaults to lib.hm.strings.toSnakeCase.
|
|
*/
|
|
mkSettingsRenamedOptionModules =
|
|
oldPrefix: newPrefix:
|
|
{
|
|
transform ? lib.hm.strings.toSnakeCase,
|
|
}:
|
|
map (
|
|
spec:
|
|
let
|
|
finalSpec =
|
|
if lib.isAttrs spec then
|
|
lib.mapAttrs (_: lib.toList) spec
|
|
else
|
|
{
|
|
old = lib.toList spec;
|
|
new = map transform finalSpec.old;
|
|
};
|
|
in
|
|
lib.mkRenamedOptionModule (oldPrefix ++ finalSpec.old) (newPrefix ++ finalSpec.new)
|
|
);
|
|
|
|
/*
|
|
Recursively transforms attribute set keys, issuing a warning for each transformation.
|
|
|
|
The function takes an attribute set with the following keys:
|
|
- pred: (str -> bool) Predicate to detect which keys to transform.
|
|
- transform: (str -> str) Function to transform the key.
|
|
- ignore: (list of str) Optional. A list of keys to never transform,
|
|
even if they match `pred`.
|
|
|
|
Example:
|
|
let
|
|
# Renames camelCase keys to snake_case.
|
|
migrateCamelCase = lib.hm.deprecations.remapAttrsRecursive {
|
|
# A key needs migration if it contains a lowercase letter followed by an uppercase one.
|
|
pred = key: builtins.match ".*[a-z][A-Z].*" key != null;
|
|
# The transformation to apply.
|
|
transform = lib.hm.strings.toSnakeCase;
|
|
# Keys we will not rename
|
|
ignore = [ "allowThisOne" ];
|
|
};
|
|
in
|
|
migrateCamelCase "programs.mymodule.settings" {
|
|
someSetting = 1; # will be renamed
|
|
allowThisOne = 2; # will be ignored
|
|
}
|
|
# => { some_setting = 1; allowThisOne = 2; }
|
|
*/
|
|
remapAttrsRecursive =
|
|
{
|
|
pred,
|
|
transform,
|
|
ignore ? [ ],
|
|
}:
|
|
let
|
|
migrate =
|
|
path: value:
|
|
if builtins.isAttrs value then
|
|
lib.mapAttrs' (
|
|
name: val:
|
|
let
|
|
newName = if pred name && !(builtins.elem name ignore) then transform name else name;
|
|
|
|
warnOrId =
|
|
if newName != name then
|
|
lib.warn "home-manager: The setting '${name}' in '${path}' was automatically renamed to '${newName}'. Please update your configuration."
|
|
else
|
|
x: x;
|
|
in
|
|
warnOrId {
|
|
name = newName;
|
|
value = migrate "${path}.${name}" val;
|
|
}
|
|
) value
|
|
else if builtins.isList value then
|
|
lib.imap0 (index: val: migrate "${path}.[${toString index}]" val) value
|
|
else
|
|
value;
|
|
in
|
|
pathStr: attrs: migrate pathStr attrs;
|
|
|
|
/*
|
|
Builds the state-version migration values for options whose defaults change
|
|
based on `home.stateVersion`.
|
|
|
|
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;
|
|
since = "26.05";
|
|
optionPath = [ "programs" "example" "foo" ];
|
|
legacy.value = "old";
|
|
current.value = "new";
|
|
};
|
|
in
|
|
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 =
|
|
{
|
|
stateVersion,
|
|
since,
|
|
optionPath,
|
|
legacy,
|
|
current,
|
|
extraWarning ? "",
|
|
config ? null,
|
|
options ? null,
|
|
warningPriority ? (lib.mkOptionDefault { }).priority,
|
|
shouldWarn ? null,
|
|
deferWarningToConfig ? false,
|
|
}:
|
|
let
|
|
option = lib.showOption optionPath;
|
|
legacyText = legacy.text or (lib.generators.toPretty { } legacy.value);
|
|
currentText = current.text or (lib.generators.toPretty { } current.value);
|
|
warning = ''
|
|
The default value of `${option}` has changed from `${legacyText}` to `${currentText}`.
|
|
You are currently using the legacy default (`${legacyText}`) because `home.stateVersion` is less than "${since}".
|
|
To silence this warning and keep legacy behavior, set:
|
|
${option} = ${legacyText};
|
|
To adopt the new default behavior, set:
|
|
${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;
|
|
valuesEqual = legacy.value == current.value;
|
|
in
|
|
assert lib.assertMsg (!deferWarningToConfig || canDeferWarning) ''
|
|
`lib.hm.deprecations.mkStateVersionOptionDefault` requires both `config` and `options`
|
|
when `deferWarningToConfig = true`.
|
|
'';
|
|
{
|
|
default =
|
|
if usingLegacyBranch && !deferWarningToConfig && !valuesEqual 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
|
|
&& !valuesEqual
|
|
&& (
|
|
if lib.isFunction shouldWarn then
|
|
shouldWarn { inherit optionInfo optionUsesDefaultPriority; }
|
|
else
|
|
optionUsesDefaultPriority
|
|
);
|
|
};
|
|
|
|
/*
|
|
Builds a predicate for list-entry warnings where an omitted field should
|
|
trigger only when another field is present with a meaningful value.
|
|
|
|
Example:
|
|
shouldWarnEntry = lib.hm.deprecations.mkListEntryOmittedFieldPredicate {
|
|
omittedField = "type";
|
|
triggerField = "config";
|
|
};
|
|
*/
|
|
mkListEntryOmittedFieldPredicate =
|
|
{
|
|
omittedField,
|
|
triggerField,
|
|
triggerPredicate ? (value: value != null && value != ""),
|
|
}:
|
|
entry:
|
|
builtins.isAttrs entry.value
|
|
&& builtins.hasAttr triggerField entry.value
|
|
&& triggerPredicate (builtins.getAttr triggerField entry.value)
|
|
&& !(builtins.hasAttr omittedField entry.value);
|
|
|
|
/*
|
|
Builds state-version migration warnings for entries in a list option whose
|
|
items are typically submodules or attrsets.
|
|
|
|
This is intended for cases where a field inside each entry has a
|
|
state-version-dependent default, but warning from that field's option
|
|
default is too noisy. The caller supplies a predicate that determines
|
|
whether a raw entry relies on the legacy implicit behavior.
|
|
|
|
Example:
|
|
let
|
|
stateVersionDefault = lib.hm.deprecations.mkStateVersionOptionDefault {
|
|
inherit (config.home) stateVersion;
|
|
since = "26.05";
|
|
optionPath = [ "programs" "example" "entries" "ENTRY" "mode" ];
|
|
legacy.value = "old";
|
|
current.value = "new";
|
|
};
|
|
in
|
|
lib.hm.deprecations.mkStateVersionListSubmoduleWarnings {
|
|
inherit (config.home) stateVersion;
|
|
since = "26.05";
|
|
baseWarning = stateVersionDefault.warning;
|
|
definitions = options.programs.example.entries.definitionsWithLocations;
|
|
shouldWarnEntry = entry:
|
|
builtins.isAttrs entry.value
|
|
&& entry.value ? config
|
|
&& entry.value.config != null
|
|
&& !(entry.value ? mode);
|
|
describeEntry = entry: "entry `${entry.value.name}`";
|
|
};
|
|
*/
|
|
mkStateVersionListSubmoduleWarnings =
|
|
{
|
|
stateVersion,
|
|
since,
|
|
baseWarning,
|
|
definitions,
|
|
shouldWarnEntry,
|
|
describeEntry ? (_entry: "an entry"),
|
|
extraEntryWarning ? (_entry: ""),
|
|
}:
|
|
let
|
|
flattenedDefinitions = lib.concatMap (
|
|
definition:
|
|
lib.imap1 (index: value: {
|
|
inherit index value;
|
|
file = baseNameOf (toString definition.file);
|
|
}) definition.value
|
|
) definitions;
|
|
|
|
formatEntryWarning =
|
|
entry:
|
|
let
|
|
entryWarning = extraEntryWarning entry;
|
|
in
|
|
lib.concatStringsSep "\n" (
|
|
[
|
|
(lib.removeSuffix "\n" baseWarning)
|
|
"Triggered by ${describeEntry entry} defined in `${entry.file}` at list index ${toString entry.index}."
|
|
]
|
|
++ lib.optional (entryWarning != "") entryWarning
|
|
);
|
|
in
|
|
lib.optionals (lib.versionOlder stateVersion since) (
|
|
map formatEntryWarning (builtins.filter shouldWarnEntry flattenedDefinitions)
|
|
);
|
|
}
|