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) isType;
 | 
						|
 | 
						|
  # 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 = isType "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 = isType "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 = isType "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 = isType "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 = isType "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 = isType "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/isType)
 | 
						|
  #
 | 
						|
  # 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;
 | 
						|
    };
 | 
						|
 | 
						|
}
 |