465 lines
15 KiB
Nix
465 lines
15 KiB
Nix
# Nixpkgs/NixOS properties. Generalize the problem of delayable (not yet
|
|
# evaluable) properties like mkIf.
|
|
|
|
let lib = import ./default.nix; in
|
|
|
|
with { inherit (builtins) head tail; };
|
|
with import ./trivial.nix;
|
|
with import ./lists.nix;
|
|
with import ./misc.nix;
|
|
with import ./attrsets.nix;
|
|
|
|
rec {
|
|
|
|
inherit (lib) typeOf;
|
|
|
|
# Tell that nothing is defined. When properties are evaluated, this type
|
|
# is used to remove an entry. Thus if your property evaluation semantic
|
|
# implies that you have to mute the content of an attribute, then your
|
|
# property should produce this value.
|
|
isNotdef = attrs: (typeOf attrs) == "notdef";
|
|
mkNotdef = {_type = "notdef";};
|
|
|
|
# General property type, it has a property attribute and a content
|
|
# attribute. The property attribute refers to an attribute set which
|
|
# contains a _type attribute and a list of functions which are used to
|
|
# evaluate this property. The content attribute is used to stack properties
|
|
# on top of each other.
|
|
#
|
|
# The optional functions which may be contained in the property attribute
|
|
# are:
|
|
# - onDelay: run on a copied property.
|
|
# - onGlobalDelay: run on all copied properties.
|
|
# - onEval: run on an evaluated property.
|
|
# - onGlobalEval: run on a list of property stack on top of their values.
|
|
isProperty = attrs: (typeOf attrs) == "property";
|
|
mkProperty = p@{property, content, ...}: p // {
|
|
_type = "property";
|
|
};
|
|
|
|
# Go through the stack of properties and apply the function `op' on all
|
|
# property and call the function `nul' on the final value which is not a
|
|
# property. The stack is traversed in reversed order. The `op' function
|
|
# should expect a property with a content which have been modified.
|
|
#
|
|
# Warning: The `op' function expects only one argument in order to avoid
|
|
# calls to mkProperties as the argument is already a valid property which
|
|
# contains the result of the folding inside the content attribute.
|
|
foldProperty = op: nul: attrs:
|
|
if isProperty attrs then
|
|
op (attrs // {
|
|
content = foldProperty op nul attrs.content;
|
|
})
|
|
else
|
|
nul attrs;
|
|
|
|
# Simple function which can be used as the `op' argument of the
|
|
# foldProperty function. Properties that you don't want to handle can be
|
|
# ignored with the `id' function. `isSearched' is a function which should
|
|
# check the type of a property and return a boolean value. `thenFun' and
|
|
# `elseFun' are functions which behave as the `op' argument of the
|
|
# foldProperty function.
|
|
foldFilter = isSearched: thenFun: elseFun: attrs:
|
|
if isSearched attrs.property then
|
|
thenFun attrs
|
|
else
|
|
elseFun attrs;
|
|
|
|
|
|
# Move properties from the current attribute set to the attribute
|
|
# contained in this attribute set. This trigger property handlers called
|
|
# `onDelay' and `onGlobalDelay'.
|
|
delayPropertiesWithIter = iter: path: attrs:
|
|
let cleanAttrs = rmProperties attrs; in
|
|
if isProperty attrs then
|
|
iter (a: v:
|
|
lib.addErrorContext "while moving properties on the attribute `${a}':" (
|
|
triggerPropertiesGlobalDelay a (
|
|
triggerPropertiesDelay a (
|
|
copyProperties attrs v
|
|
)))) path cleanAttrs
|
|
else
|
|
attrs;
|
|
|
|
delayProperties = # implicit attrs argument.
|
|
let
|
|
# mapAttrs except that it also recurse into potential mkMerge
|
|
# functions. This may cause a strictness issue because looking the
|
|
# type of a string implies evaluating it.
|
|
iter = fun: path: value:
|
|
lib.mapAttrs (attr: val:
|
|
if isProperty val && isMerge val.property then
|
|
val // { content = map (fun attr) val.content; }
|
|
else
|
|
fun attr val
|
|
) value;
|
|
in
|
|
delayPropertiesWithIter iter "";
|
|
|
|
# Call onDelay functions.
|
|
triggerPropertiesDelay = name: attrs:
|
|
let
|
|
callOnDelay = p@{property, ...}:
|
|
if property ? onDelay then
|
|
property.onDelay name p
|
|
else
|
|
p;
|
|
in
|
|
foldProperty callOnDelay id attrs;
|
|
|
|
# Call onGlobalDelay functions.
|
|
triggerPropertiesGlobalDelay = name: attrs:
|
|
let
|
|
globalDelayFuns = uniqListExt {
|
|
getter = property: property._type;
|
|
inputList = foldProperty (p@{property, content, ...}:
|
|
if property ? onGlobalDelay then
|
|
[ property ] ++ content
|
|
else
|
|
content
|
|
) (a: []) attrs;
|
|
};
|
|
|
|
callOnGlobalDelay = property: content:
|
|
property.onGlobalDelay name content;
|
|
in
|
|
fold callOnGlobalDelay attrs globalDelayFuns;
|
|
|
|
# Expect a list of values which may have properties and return the same
|
|
# list of values where all properties have been evaluated and where all
|
|
# ignored values are removed. This trigger property handlers called
|
|
# `onEval' and `onGlobalEval'.
|
|
evalProperties = valList:
|
|
if valList != [] then
|
|
filter (x: !isNotdef x) (
|
|
triggerPropertiesGlobalEval (
|
|
evalLocalProperties valList
|
|
)
|
|
)
|
|
else
|
|
valList;
|
|
|
|
evalLocalProperties = valList:
|
|
filter (x: !isNotdef x) (
|
|
map triggerPropertiesEval valList
|
|
);
|
|
|
|
# Call onEval function
|
|
triggerPropertiesEval = val:
|
|
foldProperty (p@{property, ...}:
|
|
if property ? onEval then
|
|
property.onEval p
|
|
else
|
|
p
|
|
) id val;
|
|
|
|
# Call onGlobalEval function
|
|
triggerPropertiesGlobalEval = valList:
|
|
let
|
|
globalEvalFuns = uniqListExt {
|
|
getter = property: property._type;
|
|
inputList =
|
|
fold (attrs: list:
|
|
foldProperty (p@{property, content, ...}:
|
|
if property ? onGlobalEval then
|
|
[ property ] ++ content
|
|
else
|
|
content
|
|
) (a: list) attrs
|
|
) [] valList;
|
|
};
|
|
|
|
callOnGlobalEval = property: valList: property.onGlobalEval valList;
|
|
in
|
|
fold callOnGlobalEval valList globalEvalFuns;
|
|
|
|
# Remove all properties on top of a value and return the value.
|
|
rmProperties =
|
|
foldProperty (p@{content, ...}: content) id;
|
|
|
|
# Copy properties defined on a value on another value.
|
|
copyProperties = attrs: newAttrs:
|
|
foldProperty id (x: newAttrs) attrs;
|
|
|
|
/* Merge. */
|
|
|
|
# Create "merge" statement which is skipped by the delayProperty function
|
|
# and interpreted by the underlying system using properties (modules).
|
|
|
|
# Create a "Merge" property which only contains a condition.
|
|
isMerge = attrs: (typeOf attrs) == "merge";
|
|
mkMerge = content: mkProperty {
|
|
property = {
|
|
_type = "merge";
|
|
onDelay = name: val: throw "mkMerge is not the first of the list of properties.";
|
|
onEval = val: throw "mkMerge is not allowed on option definitions.";
|
|
};
|
|
inherit content;
|
|
};
|
|
|
|
/* If. ThenElse. Always. */
|
|
|
|
# create "if" statement that can be delayed on sets until a "then-else" or
|
|
# "always" set is reached. When an always set is reached the condition
|
|
# is ignore.
|
|
|
|
# Create a "If" property which only contains a condition.
|
|
isIf = attrs: (typeOf attrs) == "if";
|
|
mkIf = condition: content: mkProperty {
|
|
property = {
|
|
_type = "if";
|
|
onGlobalDelay = onIfGlobalDelay;
|
|
onEval = onIfEval;
|
|
inherit condition;
|
|
};
|
|
inherit content;
|
|
};
|
|
|
|
mkAssert = assertion: message: content:
|
|
mkIf
|
|
(if assertion then true else throw "\nFailed assertion: ${message}")
|
|
content;
|
|
|
|
# Evaluate the "If" statements when either "ThenElse" or "Always"
|
|
# statement is encountered. Otherwise it removes multiple If statements and
|
|
# replaces them by one "If" statement where the condition is the list of all
|
|
# conditions joined with a "and" operation.
|
|
onIfGlobalDelay = name: content:
|
|
let
|
|
# extract if statements and non-if statements and repectively put them
|
|
# in the attribute list and attrs.
|
|
ifProps =
|
|
foldProperty
|
|
(foldFilter (p: isIf p)
|
|
# then, push the condition inside the list list
|
|
(p@{property, content, ...}:
|
|
{ inherit (content) attrs;
|
|
list = [property] ++ content.list;
|
|
}
|
|
)
|
|
# otherwise, add the propertie.
|
|
(p@{property, content, ...}:
|
|
{ inherit (content) list;
|
|
attrs = p // { content = content.attrs; };
|
|
}
|
|
)
|
|
)
|
|
(attrs: { list = []; inherit attrs; })
|
|
content;
|
|
|
|
# compute the list of if statements.
|
|
evalIf = content: condition: list:
|
|
if list == [] then
|
|
mkIf condition content
|
|
else
|
|
let p = head list; in
|
|
evalIf content (condition && p.condition) (tail list);
|
|
in
|
|
evalIf ifProps.attrs true ifProps.list;
|
|
|
|
# Evaluate the condition of the "If" statement to either get the value or
|
|
# to ignore the value.
|
|
onIfEval = p@{property, content, ...}:
|
|
if property.condition then
|
|
content
|
|
else
|
|
mkNotdef;
|
|
|
|
/* mkOverride */
|
|
|
|
# Create an "Override" statement which allow the user to define
|
|
# priorities between values. The default priority is 100. The lowest
|
|
# priorities are kept. The template argument must reproduce the same
|
|
# attribute set hierarchy to override leaves of the hierarchy.
|
|
isOverride = attrs: (typeOf attrs) == "override";
|
|
mkOverrideTemplate = priority: template: content: mkProperty {
|
|
property = {
|
|
_type = "override";
|
|
onDelay = onOverrideDelay;
|
|
onGlobalEval = onOverrideGlobalEval;
|
|
inherit priority template;
|
|
};
|
|
inherit content;
|
|
};
|
|
|
|
# Like mkOverrideTemplate, but without the template argument.
|
|
mkOverride = priority: content: mkOverrideTemplate priority {} content;
|
|
|
|
# Sugar to override the default value of the option by making a new
|
|
# default value based on the configuration.
|
|
mkDefaultValue = mkOverride 1000;
|
|
mkDefault = mkOverride 1000;
|
|
mkForce = mkOverride 50;
|
|
mkStrict = mkOverride 0;
|
|
|
|
# Make the template traversal in function of the property traversal. If
|
|
# the template define a non-empty attribute set, then the property is
|
|
# copied only on all mentionned attributes inside the template.
|
|
# Otherwise, the property is kept on all sub-attribute definitions.
|
|
onOverrideDelay = name: p@{property, content, ...}:
|
|
let inherit (property) template; in
|
|
if isAttrs template && template != {} then
|
|
if hasAttr name template then
|
|
p // {
|
|
property = p.property // {
|
|
template = builtins.getAttr name template;
|
|
};
|
|
}
|
|
# Do not override the attribute \name\
|
|
else
|
|
content
|
|
# Override values defined inside the attribute \name\.
|
|
else
|
|
p;
|
|
|
|
# Keep values having lowest priority numbers only throwing away those having
|
|
# a higher priority assigned.
|
|
onOverrideGlobalEval = valList:
|
|
let
|
|
defaultPrio = 100;
|
|
|
|
inherit (builtins) lessThan;
|
|
|
|
getPrioVal =
|
|
foldProperty
|
|
(foldFilter isOverride
|
|
(p@{property, content, ...}:
|
|
if content ? priority && lessThan content.priority property.priority then
|
|
content
|
|
else
|
|
content // {
|
|
inherit (property) priority;
|
|
}
|
|
)
|
|
(p@{property, content, ...}:
|
|
content // {
|
|
value = p // { content = content.value; };
|
|
}
|
|
)
|
|
) (value: { inherit value; });
|
|
|
|
addDefaultPrio = x:
|
|
if x ? priority then x
|
|
else x // { priority = defaultPrio; };
|
|
|
|
prioValList = map (x: addDefaultPrio (getPrioVal x)) valList;
|
|
|
|
higherPrio =
|
|
if prioValList == [] then
|
|
defaultPrio
|
|
else
|
|
fold (x: min:
|
|
if lessThan x.priority min then
|
|
x.priority
|
|
else
|
|
min
|
|
) (head prioValList).priority (tail prioValList);
|
|
in
|
|
map (x:
|
|
if x.priority == higherPrio then
|
|
x.value
|
|
else
|
|
mkNotdef
|
|
) prioValList;
|
|
|
|
/* mkOrder */
|
|
|
|
# Order definitions based on there index value. This property is useful
|
|
# when the result of the merge function depends on the order on the
|
|
# initial list. (e.g. concatStrings) Definitions are ordered based on
|
|
# their rank. The lowest ranked definition would be the first to element
|
|
# of the list used by the merge function. And the highest ranked
|
|
# definition would be the last. Definitions which does not have any rank
|
|
# value have the default rank of 100.
|
|
isOrder = attrs: (typeOf attrs) == "order";
|
|
mkOrder = rank: content: mkProperty {
|
|
property = {
|
|
_type = "order";
|
|
onGlobalEval = onOrderGlobalEval;
|
|
inherit rank;
|
|
};
|
|
inherit content;
|
|
};
|
|
|
|
mkHeader = mkOrder 10;
|
|
mkFooter = mkOrder 1000;
|
|
|
|
# Fetch the rank of each definition (add the default rank is none) and
|
|
# sort them based on their ranking.
|
|
onOrderGlobalEval = valList:
|
|
let
|
|
defaultRank = 100;
|
|
|
|
inherit (builtins) lessThan;
|
|
|
|
getRankVal =
|
|
foldProperty
|
|
(foldFilter isOrder
|
|
(p@{property, content, ...}:
|
|
if content ? rank then
|
|
content
|
|
else
|
|
content // {
|
|
inherit (property) rank;
|
|
}
|
|
)
|
|
(p@{property, content, ...}:
|
|
content // {
|
|
value = p // { content = content.value; };
|
|
}
|
|
)
|
|
) (value: { inherit value; });
|
|
|
|
addDefaultRank = x:
|
|
if x ? rank then x
|
|
else x // { rank = defaultRank; };
|
|
|
|
rankValList = map (x: addDefaultRank (getRankVal x)) valList;
|
|
|
|
cmp = x: y:
|
|
builtins.lessThan x.rank y.rank;
|
|
in
|
|
map (x: x.value) (sort cmp rankValList);
|
|
|
|
/* mkFixStrictness */
|
|
|
|
# This is a hack used to restore laziness on some option definitions.
|
|
# Some option definitions are evaluated when they are not used. This
|
|
# error is caused by the strictness of type checking builtins. Builtins
|
|
# like 'isAttrs' are too strict because they have to evaluate their
|
|
# arguments to check if the type is correct. This evaluation, cause the
|
|
# strictness of properties.
|
|
#
|
|
# Properties can be stacked on top of each other. The stackability of
|
|
# properties on top of the option definition is nice for user manipulation
|
|
# but require to check if the content of the property is not another
|
|
# property. Such testing implies to verify if this is an attribute set
|
|
# and if it possess the type 'property'. (see isProperty & typeOf)
|
|
#
|
|
# To avoid strict evaluation of option definitions, 'mkFixStrictness' is
|
|
# introduced. This property protects an option definition by replacing
|
|
# the base of the stack of properties by 'mkNotDef', when this property is
|
|
# evaluated it returns the original definition.
|
|
#
|
|
# This property is useful over any elements which depends on options which
|
|
# are raising errors when they get evaluated without the proper settings.
|
|
#
|
|
# Plain list and attribute set are lazy structures, which means that the
|
|
# container gets evaluated but not the content. Thus, using this property
|
|
# on top of plain list or attribute set is pointless.
|
|
#
|
|
# This is a Hack, you should avoid it!
|
|
|
|
# This property has a long name because you should avoid it.
|
|
isFixStrictness = attrs: (typeOf attrs) == "fix-strictness";
|
|
mkFixStrictness = value:
|
|
mkProperty {
|
|
property = {
|
|
_type = "fix-strictness";
|
|
onEval = p: value;
|
|
};
|
|
content = mkNotdef;
|
|
};
|
|
|
|
}
|