chore: define scopes by dirname, not manually

Before this commit, there existed slight confusion between the directory
name and the library scope name. For example, `lib.points.fix` is
defined in `points/default.nix` under an attribute set named `points`.
Theoretically, one could create a new directory, such as `foo`, but name
the library scope `bar`.

With this commit, the library scope is inferred from dirname
automatically. This also has the benefit of slightly reducing nesting
and reducing duplication of data. As a downside, it turns `files`, a
list of paths, into a list of strings. This is necessary because when
paths are parsed in NixLang, they become absolute paths, and so
`builtins.toString ./lists` would become "/home/foo/lib/src/lists",
which is undesirable. This is a downside, because it makes using
filepath LSP (like the one Neovim usually ships with, where it provides
autocomplete for paths) impossible, and so typos become a little easier
to make.

This is necessary for future work on migrating to RFC 145 style
doc-comments. This migration would allow us to build API documentation,
but first, the nesting needs to be removed, because `nixdoc` (an utility
to build Markdown out of doc-comments) doesn't expect it and generates
empty entries.
This commit is contained in:
KFears 2025-09-09 03:51:43 +04:00
parent 2af4e9f1d5
commit 683b6cb609
Signed by: KFearsoff
SSH key fingerprint: SHA256:sn39ts1xvi/KGQNlqDyrj8nNlhp9ilndgIolXKnPraw
22 changed files with 3735 additions and 3735 deletions

View file

@ -1,216 +1,214 @@
lib: {
attrs = {
## Merge two attribute sets at the base level.
##
## @type Attrs a b c => a -> b -> c
merge = x: y: x // y;
## Merge two attribute sets at the base level.
##
## @type Attrs a b c => a -> b -> c
merge = x: y: x // y;
## Merge two attribute sets recursively until a given predicate returns true.
## Any values that are _not_ attribute sets will be overridden with the value
## from `y` first if it exists and then `x` otherwise.
##
## @type Attrs a b c => (String -> Any -> Any -> Bool) -> a -> b -> c
mergeRecursiveUntil =
predicate: x: y:
let
process =
path:
builtins.zipAttrsWith (
name: values:
let
currentPath = path ++ [ name ];
isSingleValue = builtins.length values == 1;
isComplete = predicate currentPath (builtins.elemAt values 1) (builtins.elemAt values 0);
in
if isSingleValue then
builtins.elemAt values 0
else if isComplete then
builtins.elemAt values 1
else
process currentPath values
);
in
process
[ ]
[
x
y
];
## Merge two attribute sets recursively. Any values that are _not_ attribute sets
## will be overridden with the value from `y` first if it exists and then `x`
## otherwise.
##
## @type Attrs a b c => a -> b -> c
mergeRecursive = lib.attrs.mergeRecursiveUntil (
path: x: y:
!(builtins.isAttrs x && builtins.isAttrs y)
);
## Get a value from an attribute set by a path. If the path does not exist,
## a fallback value will be returned instead.
##
## @type (List String) -> a -> Attrs -> a | b
select =
path: fallback: target:
let
name = builtins.head path;
rest = builtins.tail path;
in
if path == [ ] then
target
else if target ? ${name} then
lib.attrs.select rest fallback target.${name}
else
fallback;
## Get a value from an attribute set by a path. If the path does not exist,
## an error will be thrown.
##
## @type (List String) -> Attrs -> a
selectOrThrow =
path: target:
let
pathAsString = builtins.concatStringsSep "." path;
error = builtins.throw "Path not found in attribute set: ${pathAsString}";
in
if lib.attrs.has path target then lib.attrs.select path null target else error;
## Zip specific attributes from a list of attribute sets.
##
## @type List String -> (List Any -> Any) -> List Attrs -> Attrs
zipWithNames =
names: f: list:
let
transform = name: {
inherit name;
value = f name (builtins.catAttrs name list);
};
results = builtins.map transform names;
in
builtins.listToAttrs results;
## Match an attribute set against a pattern.
##
## @type Attrs -> Attrs -> Bool
match =
pattern: value:
let
process =
## Merge two attribute sets recursively until a given predicate returns true.
## Any values that are _not_ attribute sets will be overridden with the value
## from `y` first if it exists and then `x` otherwise.
##
## @type Attrs a b c => (String -> Any -> Any -> Bool) -> a -> b -> c
mergeRecursiveUntil =
predicate: x: y:
let
process =
path:
builtins.zipAttrsWith (
name: values:
let
first = builtins.elemAt values 0;
second = builtins.elemAt values 1;
currentPath = path ++ [ name ];
isSingleValue = builtins.length values == 1;
isComplete = predicate currentPath (builtins.elemAt values 1) (builtins.elemAt values 0);
in
if builtins.length values == 1 then
false
else if builtins.isAttrs first then
builtins.isAttrs second && lib.attrs.match first second
if isSingleValue then
builtins.elemAt values 0
else if isComplete then
builtins.elemAt values 1
else
first == second;
process currentPath values
);
in
process
[ ]
[
x
y
];
result = lib.attrs.zipWithNames (builtins.attrNames pattern) process [
pattern
value
];
in
assert lib.errors.trace (builtins.isAttrs pattern) "Pattern must be an attribute set";
assert lib.errors.trace (builtins.isAttrs value) "Value must be an attribute set";
builtins.all lib.fp.id (builtins.attrValues result);
## Merge two attribute sets recursively. Any values that are _not_ attribute sets
## will be overridden with the value from `y` first if it exists and then `x`
## otherwise.
##
## @type Attrs a b c => a -> b -> c
mergeRecursive = lib.attrs.mergeRecursiveUntil (
path: x: y:
!(builtins.isAttrs x && builtins.isAttrs y)
);
## Create a nested attribute set with a value as the leaf node.
##
## @type (List String) -> a -> Attrs
set =
path: value:
let
length = builtins.length path;
process =
depth: if depth == length then value else { ${builtins.elemAt path depth} = process (depth + 1); };
in
process 0;
## Get a value from an attribute set by a path. If the path does not exist,
## a fallback value will be returned instead.
##
## @type (List String) -> a -> Attrs -> a | b
select =
path: fallback: target:
let
name = builtins.head path;
rest = builtins.tail path;
in
if path == [ ] then
target
else if target ? ${name} then
lib.attrs.select rest fallback target.${name}
else
fallback;
## Check if a path exists in an attribute set.
##
## @type (List String) -> Attrs -> Bool
has =
path: target:
let
name = builtins.head path;
rest = builtins.tail path;
in
if path == [ ] then
true
else if target ? ${name} then
lib.attrs.has rest target.${name}
else
false;
## Get a value from an attribute set by a path. If the path does not exist,
## an error will be thrown.
##
## @type (List String) -> Attrs -> a
selectOrThrow =
path: target:
let
pathAsString = builtins.concatStringsSep "." path;
error = builtins.throw "Path not found in attribute set: ${pathAsString}";
in
if lib.attrs.has path target then lib.attrs.select path null target else error;
## Depending on a given condition, either use the given value or an empty
## attribute set.
##
## @type Attrs a b => Bool -> a -> a | b
when = condition: value: if condition then value else { };
## Zip specific attributes from a list of attribute sets.
##
## @type List String -> (List Any -> Any) -> List Attrs -> Attrs
zipWithNames =
names: f: list:
let
transform = name: {
inherit name;
value = f name (builtins.catAttrs name list);
};
results = builtins.map transform names;
in
builtins.listToAttrs results;
## Map an attribute set's names and values to a list.
##
## @type Any a => (String -> Any -> a) -> Attrs -> List a
mapToList = f: target: builtins.map (name: f name target.${name}) (builtins.attrNames target);
## Match an attribute set against a pattern.
##
## @type Attrs -> Attrs -> Bool
match =
pattern: value:
let
process =
name: values:
let
first = builtins.elemAt values 0;
second = builtins.elemAt values 1;
in
if builtins.length values == 1 then
false
else if builtins.isAttrs first then
builtins.isAttrs second && lib.attrs.match first second
else
first == second;
## Map an attribute set recursively. Only non-set leaf nodes will be mapped.
##
## @type (List String -> Any -> Any) -> Attrs -> Attrs
mapRecursive = f: target: lib.attrs.mapRecursiveWhen (lib.fp.const true) f target;
result = lib.attrs.zipWithNames (builtins.attrNames pattern) process [
pattern
value
];
in
assert lib.errors.trace (builtins.isAttrs pattern) "Pattern must be an attribute set";
assert lib.errors.trace (builtins.isAttrs value) "Value must be an attribute set";
builtins.all lib.fp.id (builtins.attrValues result);
## Map an attribute set recursively when a given predicate returns true.
## Only leaf nodes according to the predicate will be mapped.
##
## @type (Attrs -> Bool) -> (List String -> Any -> Any) -> Attrs -> Attrs
mapRecursiveWhen =
predicate: f: target:
let
process =
path:
builtins.mapAttrs (
name: value:
if builtins.isAttrs value && predicate value then
process (path ++ [ name ]) value
else
f (path ++ [ name ]) value
);
in
process [ ] target;
## Create a nested attribute set with a value as the leaf node.
##
## @type (List String) -> a -> Attrs
set =
path: value:
let
length = builtins.length path;
process =
depth: if depth == length then value else { ${builtins.elemAt path depth} = process (depth + 1); };
in
process 0;
## Filter an attribute set by a given predicate. The filter is only performed
## on the base level of the attribute set.
##
## @type (String -> Any -> Bool) -> Attrs -> Attrs
filter =
predicate: target:
let
names = builtins.attrNames target;
process =
name:
let
value = target.${name};
in
if predicate name value then [ { inherit name value; } ] else [ ];
valid = builtins.concatMap process names;
in
builtins.listToAttrs valid;
## Check if a path exists in an attribute set.
##
## @type (List String) -> Attrs -> Bool
has =
path: target:
let
name = builtins.head path;
rest = builtins.tail path;
in
if path == [ ] then
true
else if target ? ${name} then
lib.attrs.has rest target.${name}
else
false;
## Generate an attribute set from a list of names and a function that is
## applied to each name.
##
## @type (List String) -> (String -> Any) -> Attrs
generate =
names: f:
let
pairs = builtins.map (name: {
inherit name;
value = f name;
}) names;
in
builtins.listToAttrs pairs;
};
## Depending on a given condition, either use the given value or an empty
## attribute set.
##
## @type Attrs a b => Bool -> a -> a | b
when = condition: value: if condition then value else { };
## Map an attribute set's names and values to a list.
##
## @type Any a => (String -> Any -> a) -> Attrs -> List a
mapToList = f: target: builtins.map (name: f name target.${name}) (builtins.attrNames target);
## Map an attribute set recursively. Only non-set leaf nodes will be mapped.
##
## @type (List String -> Any -> Any) -> Attrs -> Attrs
mapRecursive = f: target: lib.attrs.mapRecursiveWhen (lib.fp.const true) f target;
## Map an attribute set recursively when a given predicate returns true.
## Only leaf nodes according to the predicate will be mapped.
##
## @type (Attrs -> Bool) -> (List String -> Any -> Any) -> Attrs -> Attrs
mapRecursiveWhen =
predicate: f: target:
let
process =
path:
builtins.mapAttrs (
name: value:
if builtins.isAttrs value && predicate value then
process (path ++ [ name ]) value
else
f (path ++ [ name ]) value
);
in
process [ ] target;
## Filter an attribute set by a given predicate. The filter is only performed
## on the base level of the attribute set.
##
## @type (String -> Any -> Bool) -> Attrs -> Attrs
filter =
predicate: target:
let
names = builtins.attrNames target;
process =
name:
let
value = target.${name};
in
if predicate name value then [ { inherit name value; } ] else [ ];
valid = builtins.concatMap process names;
in
builtins.listToAttrs valid;
## Generate an attribute set from a list of names and a function that is
## applied to each name.
##
## @type (List String) -> (String -> Any) -> Attrs
generate =
names: f:
let
pairs = builtins.map (name: {
inherit name;
value = f name;
}) names;
in
builtins.listToAttrs pairs;
}

View file

@ -1,48 +1,46 @@
lib: {
bools = {
into = {
## Convert a boolean value into a string.
##
## @type Bool -> String
string = value: if value then "true" else "false";
## Convert a boolean into either the string "yes" or "no".
##
## @type Bool -> String
yesno = value: if value then "yes" else "no";
};
## Choose between two values based on a condition. When true, the first value
## is returned, otherwise the second value is returned.
into = {
## Convert a boolean value into a string.
##
## @type Bool -> a -> b -> a | b
when =
condition: x: y:
if condition then x else y;
## @type Bool -> String
string = value: if value then "true" else "false";
## Perform a logical AND operation on two values.
## Convert a boolean into either the string "yes" or "no".
##
## @type Bool -> Bool -> Bool
and = a: b: a && b;
## Perform a logical AND operation on two functions being applied to a value.
##
## @type (a -> Bool) -> (a -> Bool) -> a -> Bool
and' = f: g: (x: (f x) && (g x));
## Perform a logical OR operation on two values.
##
## @type Bool -> Bool -> Bool
or = a: b: a || b;
## Perform a logical OR operation on two functions being applied to a value.
##
## @type (a -> Bool) -> (a -> Bool) -> a -> Bool
or' = f: g: (x: (f x) || (g x));
## Perform a logical NOT operation on a value.
##
## @type Bool -> Bool
not = a: !a;
## @type Bool -> String
yesno = value: if value then "yes" else "no";
};
## Choose between two values based on a condition. When true, the first value
## is returned, otherwise the second value is returned.
##
## @type Bool -> a -> b -> a | b
when =
condition: x: y:
if condition then x else y;
## Perform a logical AND operation on two values.
##
## @type Bool -> Bool -> Bool
and = a: b: a && b;
## Perform a logical AND operation on two functions being applied to a value.
##
## @type (a -> Bool) -> (a -> Bool) -> a -> Bool
and' = f: g: (x: (f x) && (g x));
## Perform a logical OR operation on two values.
##
## @type Bool -> Bool -> Bool
or = a: b: a || b;
## Perform a logical OR operation on two functions being applied to a value.
##
## @type (a -> Bool) -> (a -> Bool) -> a -> Bool
or' = f: g: (x: (f x) || (g x));
## Perform a logical NOT operation on a value.
##
## @type Bool -> Bool
not = a: !a;
}

View file

@ -1,155 +1,153 @@
lib: {
dag = {
validate = {
## Check that a value is a DAG entry.
##
## @type a -> Bool
entry = value: (value ? value) && (value ? before) && (value ? after);
validate = {
## Check that a value is a DAG entry.
##
## @type a -> Bool
entry = value: (value ? value) && (value ? before) && (value ? after);
## Check that a value is a DAG.
##
## @type a -> Bool
graph =
value:
let
isContentsValid = builtins.all lib.dag.validate.entry (builtins.attrValues value);
in
builtins.isAttrs value && isContentsValid;
};
## Check that a value is a DAG.
##
## @type a -> Bool
graph =
value:
let
isContentsValid = builtins.all lib.dag.validate.entry (builtins.attrValues value);
in
builtins.isAttrs value && isContentsValid;
};
sort = {
## Apply a topological sort to a DAG.
##
## @type Dag a -> { result :: List a } | { cycle :: List a, loops :: List a }
topological =
graph:
let
getEntriesBefore =
graph: name:
let
before = lib.attrs.filter (key: value: builtins.elem name value.before) graph;
in
builtins.attrNames before;
sort = {
## Apply a topological sort to a DAG.
##
## @type Dag a -> { result :: List a } | { cycle :: List a, loops :: List a }
topological =
graph:
let
getEntriesBefore =
graph: name:
let
before = lib.attrs.filter (key: value: builtins.elem name value.before) graph;
in
builtins.attrNames before;
normalize = name: value: {
inherit name;
normalize = name: value: {
inherit name;
value = value.value;
after = value.after ++ (getEntriesBefore graph name);
};
normalized = builtins.mapAttrs normalize graph;
entries = builtins.attrValues normalized;
isBefore = a: b: builtins.elem a.name b.after;
sorted = lib.lists.sort.topological isBefore entries;
in
if sorted ? result then
{
result = builtins.map (value: {
name = value.name;
value = value.value;
after = value.after ++ (getEntriesBefore graph name);
};
}) sorted.result;
}
else
sorted;
};
normalized = builtins.mapAttrs normalize graph;
entries = builtins.attrValues normalized;
isBefore = a: b: builtins.elem a.name b.after;
sorted = lib.lists.sort.topological isBefore entries;
in
if sorted ? result then
{
result = builtins.map (value: {
name = value.name;
value = value.value;
}) sorted.result;
}
else
sorted;
};
apply = {
## Apply a set of defaults to a graph. This will ensure that missing entries are added
## and any entries that do exist are given the appropriate `before` and `after` values.
##
## @type Dag a -> Dag a -> Dag a
defaults =
graph: defaults:
let
result = builtins.mapAttrs (
name: entry:
if defaults ? ${name} then
if builtins.isString entry then
{
value = entry;
before = defaults.${name}.before or [ ];
after = defaults.${name}.after or [ ];
}
else
entry
// {
before = (entry.before or [ ]) ++ (defaults.${name}.before or [ ]);
after = (entry.after or [ ]) ++ (defaults.${name}.after or [ ]);
}
apply = {
## Apply a set of defaults to a graph. This will ensure that missing entries are added
## and any entries that do exist are given the appropriate `before` and `after` values.
##
## @type Dag a -> Dag a -> Dag a
defaults =
graph: defaults:
let
result = builtins.mapAttrs (
name: entry:
if defaults ? ${name} then
if builtins.isString entry then
{
value = entry;
before = defaults.${name}.before or [ ];
after = defaults.${name}.after or [ ];
}
else
entry
) graph;
in
defaults // result;
};
// {
before = (entry.before or [ ]) ++ (defaults.${name}.before or [ ]);
after = (entry.after or [ ]) ++ (defaults.${name}.after or [ ]);
}
else
entry
) graph;
in
defaults // result;
};
## Map over the entries in a DAG and modify their values.
## Map over the entries in a DAG and modify their values.
##
## @type (String -> a -> b) -> Dag a -> Dag b
map = f: builtins.mapAttrs (name: value: value // { value = f name value.value; });
entry = {
## Create a new DAG entry.
##
## @type (String -> a -> b) -> Dag a -> Dag b
map = f: builtins.mapAttrs (name: value: value // { value = f name value.value; });
## @type List String -> List String -> a -> { before :: List String, after :: List String, value :: a }
between = after: before: value: { inherit before after value; };
entry = {
## Create a new DAG entry.
##
## @type List String -> List String -> a -> { before :: List String, after :: List String, value :: a }
between = after: before: value: { inherit before after value; };
## Create a new DAG entry with no dependencies.
##
## @type a -> { before :: List String, after :: List String, value :: a }
anywhere = lib.dag.entry.between [ ] [ ];
## Create a new DAG entry with no dependencies.
##
## @type a -> { before :: List String, after :: List String, value :: a }
anywhere = lib.dag.entry.between [ ] [ ];
## Create a new DAG entry that occurs before other entries.
##
## @type List String -> a -> { before :: List String, after :: List String, value :: a }
before = before: lib.dag.entry.between [ ] before;
## Create a new DAG entry that occurs before other entries.
##
## @type List String -> a -> { before :: List String, after :: List String, value :: a }
before = before: lib.dag.entry.between [ ] before;
## Create a new DAG entry that occurs after other entries.
##
## @type List String -> a -> { before :: List String, after :: List String, value :: a }
after = after: lib.dag.entry.between after [ ];
};
## Create a new DAG entry that occurs after other entries.
##
## @type List String -> a -> { before :: List String, after :: List String, value :: a }
after = after: lib.dag.entry.between after [ ];
};
entries = {
## Create a DAG from a list of entries, prefixed with a tag.
##
## @type String -> List String -> List String -> List a -> Dag a
between =
tag:
let
process =
i: after: before: entries:
let
name = "${tag}-${builtins.toString i}";
entry = builtins.head entries;
rest = builtins.tail entries;
in
if builtins.length entries == 0 then
{ }
else if builtins.length entries == 1 then
{ "${name}" = lib.dag.entry.between after before entry; }
else
{ "${name}" = lib.dag.entry.after after entry; } // (process (i + 1) before [ name ] rest);
in
process 0;
entries = {
## Create a DAG from a list of entries, prefixed with a tag.
##
## @type String -> List String -> List String -> List a -> Dag a
between =
tag:
let
process =
i: after: before: entries:
let
name = "${tag}-${builtins.toString i}";
entry = builtins.head entries;
rest = builtins.tail entries;
in
if builtins.length entries == 0 then
{ }
else if builtins.length entries == 1 then
{ "${name}" = lib.dag.entry.between after before entry; }
else
{ "${name}" = lib.dag.entry.after after entry; } // (process (i + 1) before [ name ] rest);
in
process 0;
## Create a DAG from a list of entries, prefixed with a tag, that can occur anywhere.
##
## @type String -> List a -> Dag a
anywhere = tag: lib.dag.entries.between tag [ ] [ ];
## Create a DAG from a list of entries, prefixed with a tag, that can occur anywhere.
##
## @type String -> List a -> Dag a
anywhere = tag: lib.dag.entries.between tag [ ] [ ];
## Create a DAG from a list of entries, prefixed with a tag, that occurs before other entries.
##
## @type String -> List String -> List a -> Dag a
before = tag: before: lib.dag.entries.between tag [ ] before;
## Create a DAG from a list of entries, prefixed with a tag, that occurs before other entries.
##
## @type String -> List String -> List a -> Dag a
before = tag: before: lib.dag.entries.between tag [ ] before;
## Create a DAG from a list of entries, prefixed with a tag, that occurs after other entries.
##
## @type String -> List String -> List a -> Dag a
after = tag: after: lib.dag.entries.between tag after [ ];
};
## Create a DAG from a list of entries, prefixed with a tag, that occurs after other entries.
##
## @type String -> List String -> List a -> Dag a
after = tag: after: lib.dag.entries.between tag after [ ];
};
}

View file

@ -1,27 +1,43 @@
let
files = [
./attrs
./bools
./dag
./errors
./fp
./generators
./importers
./licenses
./lists
./math
./modules
./numbers
./options
./packages
./paths
./points
./strings
./types
./versions
"attrs"
"bools"
"dag"
"errors"
"fp"
"generators"
"importers"
"licenses"
"lists"
"math"
"modules"
"numbers"
"options"
"packages"
"paths"
"points"
"strings"
"types"
"versions"
];
libs = builtins.map (f: import f) files;
# Convert string to path
pathify = strpath: ./. + "/${strpath}";
# We need to reconstruct this:
#
# ```
# lib: { foo = "sdf"; bar = "sdfds"; }
# ```
#
# To this:
#
# ```
# lib: { filename = { foo = "sdf"; bar = "sdfds"; }; }
# ```
#
# For this, we return a new attrset with key "${filename}", and we wrap this thing into a lambda
libs = builtins.map (f: lib: { "${f}" = import (pathify f) lib; }) files;
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows

View file

@ -1,10 +1,8 @@
lib: {
errors = {
## Prints a message if the condition is not met. The result of
## the condition is returned.
##
## @notest
## @type Bool -> String -> Bool
trace = condition: message: if condition then true else builtins.trace message false;
};
## Prints a message if the condition is not met. The result of
## the condition is returned.
##
## @notest
## @type Bool -> String -> Bool
trace = condition: message: if condition then true else builtins.trace message false;
}

View file

@ -1,64 +1,62 @@
lib: {
fp = {
## A function that returns its argument.
##
## @type a -> a
id = x: x;
## A function that returns its argument.
##
## @type a -> a
id = x: x;
## Create a function that ignores its argument and returns a constant value.
##
## @type a -> b -> a
const = x: (_: x);
## Create a function that ignores its argument and returns a constant value.
##
## @type a -> b -> a
const = x: (_: x);
## Compose two functions to produce a new function that applies them both
## from right to left.
##
## @type Function f g => (b -> c) -> (a -> b) -> a -> c
compose = f: g: (x: f (g x));
## Compose two functions to produce a new function that applies them both
## from right to left.
##
## @type Function f g => (b -> c) -> (a -> b) -> a -> c
compose = f: g: (x: f (g x));
## Process a value with a series of functions. Functions are applied in the
## order they are provided.
##
## @type (List (Any -> Any)) -> Any -> Any
pipe = fs: (x: builtins.foldl' (value: f: f value) x fs);
## Process a value with a series of functions. Functions are applied in the
## order they are provided.
##
## @type (List (Any -> Any)) -> Any -> Any
pipe = fs: (x: builtins.foldl' (value: f: f value) x fs);
## Reverse the order of arguments to a function that has two parameters.
##
## @type (a -> b -> c) -> b -> a -> c
flip2 =
f: a: b:
f b a;
## Reverse the order of arguments to a function that has three parameters.
##
## @type (a -> b -> c -> d) -> c -> b -> a -> d
flip3 =
f: a: b: c:
f c b a;
## Reverse the order of arguments to a function that has four parameters.
##
## @type (a -> b -> c -> d -> e) -> d -> c -> b -> a -> e
flip4 =
f: a: b: c: d:
f d c b a;
## Reverse the order of arguments to a function that has two parameters.
##
## @type (a -> b -> c) -> b -> a -> c
flip2 =
f: a: b:
f b a;
## Reverse the order of arguments to a function that has three parameters.
##
## @type (a -> b -> c -> d) -> c -> b -> a -> d
flip3 =
f: a: b: c:
f c b a;
## Reverse the order of arguments to a function that has four parameters.
##
## @type (a -> b -> c -> d -> e) -> d -> c -> b -> a -> e
flip4 =
f: a: b: c: d:
f d c b a;
## Get the arguments of a function or functor.
## An attribute set is returned with the arguments as keys. The values
## are `true` when the argument has a default value specified and `false`
## when it does not.
##
## @type Function -> Attrs
args =
f: if f ? __functor then f.__args__ or (lib.fp.args (f.__functor f)) else builtins.functionArgs f;
## Get the arguments of a function or functor.
## An attribute set is returned with the arguments as keys. The values
## are `true` when the argument has a default value specified and `false`
## when it does not.
##
## @type Function -> Attrs
args =
f: if f ? __functor then f.__args__ or (lib.fp.args (f.__functor f)) else builtins.functionArgs f;
## Create a function that is called with only the arguments it specifies.
##
## @type Attrs a => (a -> b) -> a -> b
withDynamicArgs =
f: args:
let
fArgs = lib.fp.args f;
common = builtins.intersectAttrs fArgs args;
in
if builtins.isAttrs args then if fArgs == { } then f args else f common else f args;
};
## Create a function that is called with only the arguments it specifies.
##
## @type Attrs a => (a -> b) -> a -> b
withDynamicArgs =
f: args:
let
fArgs = lib.fp.args f;
common = builtins.intersectAttrs fArgs args;
in
if builtins.isAttrs args then if fArgs == { } then f args else f common else f args;
}

View file

@ -1,152 +1,150 @@
lib: {
generators = {
## Limit evaluation of a valud to a certain depth.
##
## @type { limit? :: Int | Null, throw? :: Bool } -> a -> a
withRecursion =
{
limit ? null,
throw ? true,
}:
assert builtins.isInt limit;
let
special = [
"__functor"
"__functionArgs"
"__toString"
"__pretty__"
];
attr = next: name: if builtins.elem name special then lib.fp.id else next;
transform =
depth:
if limit != null && depth > limit then
if throw then
builtins.throw "Exceeded maximum eval-depth limit of ${builtins.toString limit} while trying to evaluate with `lib.generators.withRecursion'!"
## Limit evaluation of a valud to a certain depth.
##
## @type { limit? :: Int | Null, throw? :: Bool } -> a -> a
withRecursion =
{
limit ? null,
throw ? true,
}:
assert builtins.isInt limit;
let
special = [
"__functor"
"__functionArgs"
"__toString"
"__pretty__"
];
attr = next: name: if builtins.elem name special then lib.fp.id else next;
transform =
depth:
if limit != null && depth > limit then
if throw then
builtins.throw "Exceeded maximum eval-depth limit of ${builtins.toString limit} while trying to evaluate with `lib.generators.withRecursion'!"
else
lib.fp.const "<unevaluated>"
else
lib.fp.id;
process =
depth: value:
let
next = x: process (depth + 1) (transform (depth + 1) x);
in
if builtins.isAttrs value then
builtins.mapAttrs (attr next) value
else if builtins.isList value then
builtins.map next value
else
transform (depth + 1) value;
in
process 0;
## Create a pretty printer for Nix values.
##
## @type { indent? :: String, multiline? :: Bool, allowCustomPrettifiers? :: Bool } -> a -> string
pretty =
{
indent ? "",
multiline ? true,
allowCustomPrettifiers ? false,
}:
let
process =
indent: value:
let
prefix = if multiline then "\n${indent} " else " ";
suffix = if multiline then "\n${indent}" else " ";
prettyNull = "null";
prettyNumber = lib.numbers.into.string value;
prettyBool = lib.bools.into.string value;
prettyPath = builtins.toString value;
prettyString =
let
lines = builtins.filter (x: !builtins.isList x) (builtins.split "\n" value);
escapeSingleline = lib.strings.escape.any [
"\\"
"\""
"\${"
];
escapeMultiline =
builtins.replaceStrings
[
"\${"
"''"
]
[
"''\${"
"'''"
];
singlelineResult = "\"" + lib.strings.concatMapSep "\\n" escapeSingleline lines + "\"";
multilineResult =
let
escapedLines = builtins.map escapeMultiline lines;
# The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
# indentation level. Otherwise, '' is appended to the last line.
lastLine = lib.lists.last escapedLines;
contents = builtins.concatStringsSep prefix (lib.lists.init escapedLines);
contentsSuffix = if lastLine == "" then suffix else prefix + lastLine;
in
"''" + prefix + contents + contentsSuffix + "''";
in
if multiline && builtins.length lines > 1 then multilineResult else singlelineResult;
prettyList =
let
contents = lib.strings.concatMapSep prefix (process (indent + " ")) value;
in
if builtins.length value == 0 then "[ ]" else "[${prefix}${contents}${suffix}]";
prettyFunction =
let
args = lib.fp.args value;
markArgOptional = name: default: if default then name + "?" else name;
argsWithDefaults = lib.attrs.mapToList markArgOptional args;
serializedArgs = builtins.concatStringsSep ", " argsWithDefaults;
in
if args == { } then "<function>" else "<function, args: {${serializedArgs}}>";
prettyAttrs =
let
contents = builtins.concatStringsSep prefix (
lib.attrs.mapToList (
name: value:
"${lib.strings.escape.nix.identifier name} = ${
builtins.addErrorContext "while evaluating an attribute `${name}`" (process (indent + " ") value)
};"
) value
);
in
if allowCustomPrettifiers && value ? __pretty__ && value ? value then
value.__pretty__ value.value
else if value == { } then
"{ }"
else if lib.packages.isDerivation value then
"<derivation ${value.name or "???"}>"
else
lib.fp.const "<unevaluated>"
else
lib.fp.id;
process =
depth: value:
let
next = x: process (depth + 1) (transform (depth + 1) x);
in
if builtins.isAttrs value then
builtins.mapAttrs (attr next) value
else if builtins.isList value then
builtins.map next value
else
transform (depth + 1) value;
in
process 0;
## Create a pretty printer for Nix values.
##
## @type { indent? :: String, multiline? :: Bool, allowCustomPrettifiers? :: Bool } -> a -> string
pretty =
{
indent ? "",
multiline ? true,
allowCustomPrettifiers ? false,
}:
let
process =
indent: value:
let
prefix = if multiline then "\n${indent} " else " ";
suffix = if multiline then "\n${indent}" else " ";
prettyNull = "null";
prettyNumber = lib.numbers.into.string value;
prettyBool = lib.bools.into.string value;
prettyPath = builtins.toString value;
prettyString =
let
lines = builtins.filter (x: !builtins.isList x) (builtins.split "\n" value);
escapeSingleline = lib.strings.escape.any [
"\\"
"\""
"\${"
];
escapeMultiline =
builtins.replaceStrings
[
"\${"
"''"
]
[
"''\${"
"'''"
];
singlelineResult = "\"" + lib.strings.concatMapSep "\\n" escapeSingleline lines + "\"";
multilineResult =
let
escapedLines = builtins.map escapeMultiline lines;
# The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
# indentation level. Otherwise, '' is appended to the last line.
lastLine = lib.lists.last escapedLines;
contents = builtins.concatStringsSep prefix (lib.lists.init escapedLines);
contentsSuffix = if lastLine == "" then suffix else prefix + lastLine;
in
"''" + prefix + contents + contentsSuffix + "''";
in
if multiline && builtins.length lines > 1 then multilineResult else singlelineResult;
prettyList =
let
contents = lib.strings.concatMapSep prefix (process (indent + " ")) value;
in
if builtins.length value == 0 then "[ ]" else "[${prefix}${contents}${suffix}]";
prettyFunction =
let
args = lib.fp.args value;
markArgOptional = name: default: if default then name + "?" else name;
argsWithDefaults = lib.attrs.mapToList markArgOptional args;
serializedArgs = builtins.concatStringsSep ", " argsWithDefaults;
in
if args == { } then "<function>" else "<function, args: {${serializedArgs}}>";
prettyAttrs =
let
contents = builtins.concatStringsSep prefix (
lib.attrs.mapToList (
name: value:
"${lib.strings.escape.nix.identifier name} = ${
builtins.addErrorContext "while evaluating an attribute `${name}`" (process (indent + " ") value)
};"
) value
);
in
if allowCustomPrettifiers && value ? __pretty__ && value ? value then
value.__pretty__ value.value
else if value == { } then
"{ }"
else if lib.packages.isDerivation value then
"<derivation ${value.name or "???"}>"
else
"{${prefix}${contents}${suffix}}";
in
if null == value then
prettyNull
else if builtins.isInt value || builtins.isFloat value then
prettyNumber
else if builtins.isBool value then
prettyBool
else if builtins.isString value then
prettyString
else if builtins.isPath value then
prettyPath
else if builtins.isList value then
prettyList
else if builtins.isFunction value then
prettyFunction
else if builtins.isAttrs value then
prettyAttrs
else
builtins.abort "lib.generators.pretty: should never happen (value = ${value})";
in
process indent;
};
"{${prefix}${contents}${suffix}}";
in
if null == value then
prettyNull
else if builtins.isInt value || builtins.isFloat value then
prettyNumber
else if builtins.isBool value then
prettyBool
else if builtins.isString value then
prettyString
else if builtins.isPath value then
prettyPath
else if builtins.isList value then
prettyList
else if builtins.isFunction value then
prettyFunction
else if builtins.isAttrs value then
prettyAttrs
else
builtins.abort "lib.generators.pretty: should never happen (value = ${value})";
in
process indent;
}

View file

@ -1,15 +1,13 @@
lib: {
importers = {
## Import a JSON file as a Nix value.
##
## @notest
## @type Path -> a
json = file: builtins.fromJSON (builtins.readFile file);
## Import a JSON file as a Nix value.
##
## @notest
## @type Path -> a
json = file: builtins.fromJSON (builtins.readFile file);
## Import a TOML file as a Nix value.
##
## @notest
## @type Path -> a
toml = file: builtins.fromTOML (builtins.readFile file);
};
## Import a TOML file as a Nix value.
##
## @notest
## @type Path -> a
toml = file: builtins.fromTOML (builtins.readFile file);
}

View file

@ -1,29 +1,27 @@
lib: {
licenses =
let
raw = import ./all.nix;
lib:
let
raw = import ./all.nix;
defaults = name: {
inherit name;
free = true;
};
defaults = name: {
inherit name;
free = true;
};
withDefaults = name: license: (defaults name) // license;
withSpdx =
license:
if license ? spdx then
license // { url = "https://spdx.org/licenses/${license.spdx}.html"; }
else
license;
withRedistributable = license: { redistributable = license.free; } // license;
withDefaults = name: license: (defaults name) // license;
withSpdx =
license:
if license ? spdx then
license // { url = "https://spdx.org/licenses/${license.spdx}.html"; }
else
license;
withRedistributable = license: { redistributable = license.free; } // license;
normalize =
name:
lib.fp.pipe [
(withDefaults name)
withSpdx
withRedistributable
];
in
builtins.mapAttrs normalize raw;
}
normalize =
name:
lib.fp.pipe [
(withDefaults name)
withSpdx
withRedistributable
];
in
builtins.mapAttrs normalize raw

View file

@ -1,255 +1,249 @@
lib: {
lists = {
from = {
## Convert a value to a list. If the value is already a list,
## it will be returned as-is. If the value is not a list, it
## will be wrapped in a list.
##
## @type a | (List a) -> List a
any = value: if builtins.isList value then value else [ value ];
};
sort = {
## Perform a natural sort on a list of strings.
##
## @type List String -> List String
natural =
list:
let
vectorize =
string:
let
serialize = part: if builtins.isList part then lib.strings.into.int (builtins.head part) else part;
parts = lib.strings.split "(0|[1-9][0-9]*)" string;
in
builtins.map serialize parts;
prepared = builtins.map
(value: [
(vectorize value)
value
])
list;
isLess = a: b: (lib.lists.compare lib.numbers.compare (builtins.head a) (builtins.head b)) < 0;
in
builtins.map (x: builtins.elemAt x 1) (builtins.sort isLess prepared);
## Perform a topological sort on a list of items. The predicate function determines whether
## its first argument comes before the second argument.
##
## @type (a -> a -> Bool) -> List a -> List a
topological =
predicate: list:
let
searched = lib.lists.search.depthFirst true predicate list;
results = lib.lists.sort.topological predicate (searched.visited ++ searched.rest);
in
if builtins.length list < 2 then
{ result = list; }
else if searched ? cycle then
{
loops = searched.loops;
cycle = lib.lists.reverse ([ searched.cycle ] ++ searched.visited);
}
else if results ? cycle then
results
else
{ result = [ searched.minimal ] ++ results.result; };
};
search = {
## Perform a depth first search on a list. The supplied predicate function determines whether
## its first argument comes before the second argument.
##
## @type Bool -> (a -> a -> Bool) -> List a
depthFirst =
isAcyclical: predicate: list:
let
process =
current: visited: rest:
let
loops = builtins.filter (value: predicate value current) visited;
partitioned = builtins.partition (value: predicate value current) rest;
in
if isAcyclical && (builtins.length loops > 0) then
{
cycle = current;
inherit loops visited rest;
}
else if builtins.length partitioned.right == 0 then
{
minimal = current;
inherit visited rest;
}
else
process (builtins.head partitioned.right) ([ current ] ++ visited) (
builtins.tail partitioned.right ++ partitioned.wrong
);
in
process (builtins.head list) [ ] (builtins.tail list);
};
cartesian = {
product = domain:
let
names = builtins.attrNames domain;
process =
results: name:
builtins.concatMap
(result: builtins.map (values: result // { ${name} = values; }) domain.${name})
results;
in
builtins.foldl' process [{ }] names;
map = f: domain:
builtins.map f (lib.lists.cartesian.product domain);
};
## Map a list using both the index and value of each item. The
## index starts at 0.
from = {
## Convert a value to a list. If the value is already a list,
## it will be returned as-is. If the value is not a list, it
## will be wrapped in a list.
##
## @type (Int -> a -> b) -> List a -> List b
mapWithIndex = f: list: builtins.genList (i: f i (builtins.elemAt list i)) (builtins.length list);
## Map a list using both the index and value of each item. The
## index starts at 1.
##
## @type (Int -> a -> b) -> List a -> List b
mapWithIndex1 =
f: list: builtins.genList (i: f (i + 1) (builtins.elemAt list i)) (builtins.length list);
## Compare two lists using a custom compare function. The compare
## function is called for each element in the lists that need to
## be compared.
##
## @type (a -> b -> -1 | 0 | 1) -> List a -> List b -> Int
compare =
compare: a: b:
let
result = compare (builtins.head a) (builtins.head b);
in
if a == [ ] then
if b == [ ] then 0 else -1
else if b == [ ] then
1
else if result == 0 then
lib.lists.compare compare (builtins.tail a) (builtins.tail b)
else
result;
## Get the last element of a list.
##
## @type List a -> a
last =
list:
assert lib.errors.trace (list != [ ]) "List cannot be empty";
builtins.elemAt list (builtins.length list - 1);
## Slice part of a list to create a new list.
##
## @type Int -> Int -> List -> List
slice =
start: count: list:
let
listLength = builtins.length list;
resultLength =
if start >= listLength then
0
else if start + count > listLength then
listLength - start
else
count;
in
builtins.genList (i: builtins.elemAt list (start + i)) resultLength;
## Take the first n elements of a list.
##
## @type Int -> List -> List
take = lib.lists.slice 0;
## Drop the first n elements of a list.
##
## @type Int -> List -> List
drop =
count: list:
let
listLength = builtins.length list;
in
lib.lists.slice count listLength list;
## Take all but the last element of a list.
##
## @type List -> List
init =
list:
assert lib.errors.trace (builtins.length list != 0) "lib.lists.init: list must not be empty.";
lib.lists.take (builtins.length list - 1) list;
## Reverse a list.
##
## @type List -> List
reverse =
list:
let
length = builtins.length list;
create = i: builtins.elemAt list (length - i - 1);
in
builtins.genList create length;
## Interleave a list with a separator.
##
## @type Separator -> List -> List
intersperse =
separator: list:
let
length = builtins.length list;
in
if length < 2 then
list
else
builtins.tail (
builtins.concatMap
(part: [
separator
part
])
list
);
## Create a list of integers from a starting number to an ending
## number. This *includes* the ending number as well.
##
## @type Int -> Int -> List
range = start: end: if start > end then [ ] else builtins.genList (i: start + i) (end - start + 1);
## Depending on a given condition, either use the given value (as
## a list) or an empty list.
##
## @type Attrs a b => Bool -> a -> a | b
when =
condition: value: if condition then if builtins.isList value then value else [ value ] else [ ];
## Count the number of items in a list that satisfy a given predicate.
##
## @type (a -> Bool) -> List a -> Int
count =
predicate: list:
builtins.foldl' (total: value: if predicate value then total + 1 else total) 0 list;
## Remove duplicate items from a list.
##
## @type List -> List
unique =
list:
let
filter = result: value: if builtins.elem value result then result else result ++ [ value ];
in
builtins.foldl' filter [ ] list;
## Flatten a list of lists into a single list.
##
## @type List (List a) -> List a
flatten =
value: if builtins.isList value then builtins.concatMap lib.lists.flatten value else [ value ];
## @type a | (List a) -> List a
any = value: if builtins.isList value then value else [ value ];
};
sort = {
## Perform a natural sort on a list of strings.
##
## @type List String -> List String
natural =
list:
let
vectorize =
string:
let
serialize = part: if builtins.isList part then lib.strings.into.int (builtins.head part) else part;
parts = lib.strings.split "(0|[1-9][0-9]*)" string;
in
builtins.map serialize parts;
prepared = builtins.map (value: [
(vectorize value)
value
]) list;
isLess = a: b: (lib.lists.compare lib.numbers.compare (builtins.head a) (builtins.head b)) < 0;
in
builtins.map (x: builtins.elemAt x 1) (builtins.sort isLess prepared);
## Perform a topological sort on a list of items. The predicate function determines whether
## its first argument comes before the second argument.
##
## @type (a -> a -> Bool) -> List a -> List a
topological =
predicate: list:
let
searched = lib.lists.search.depthFirst true predicate list;
results = lib.lists.sort.topological predicate (searched.visited ++ searched.rest);
in
if builtins.length list < 2 then
{ result = list; }
else if searched ? cycle then
{
loops = searched.loops;
cycle = lib.lists.reverse ([ searched.cycle ] ++ searched.visited);
}
else if results ? cycle then
results
else
{ result = [ searched.minimal ] ++ results.result; };
};
search = {
## Perform a depth first search on a list. The supplied predicate function determines whether
## its first argument comes before the second argument.
##
## @type Bool -> (a -> a -> Bool) -> List a
depthFirst =
isAcyclical: predicate: list:
let
process =
current: visited: rest:
let
loops = builtins.filter (value: predicate value current) visited;
partitioned = builtins.partition (value: predicate value current) rest;
in
if isAcyclical && (builtins.length loops > 0) then
{
cycle = current;
inherit loops visited rest;
}
else if builtins.length partitioned.right == 0 then
{
minimal = current;
inherit visited rest;
}
else
process (builtins.head partitioned.right) ([ current ] ++ visited) (
builtins.tail partitioned.right ++ partitioned.wrong
);
in
process (builtins.head list) [ ] (builtins.tail list);
};
cartesian = {
product =
domain:
let
names = builtins.attrNames domain;
process =
results: name:
builtins.concatMap (
result: builtins.map (values: result // { ${name} = values; }) domain.${name}
) results;
in
builtins.foldl' process [ { } ] names;
map = f: domain: builtins.map f (lib.lists.cartesian.product domain);
};
## Map a list using both the index and value of each item. The
## index starts at 0.
##
## @type (Int -> a -> b) -> List a -> List b
mapWithIndex = f: list: builtins.genList (i: f i (builtins.elemAt list i)) (builtins.length list);
## Map a list using both the index and value of each item. The
## index starts at 1.
##
## @type (Int -> a -> b) -> List a -> List b
mapWithIndex1 =
f: list: builtins.genList (i: f (i + 1) (builtins.elemAt list i)) (builtins.length list);
## Compare two lists using a custom compare function. The compare
## function is called for each element in the lists that need to
## be compared.
##
## @type (a -> b -> -1 | 0 | 1) -> List a -> List b -> Int
compare =
compare: a: b:
let
result = compare (builtins.head a) (builtins.head b);
in
if a == [ ] then
if b == [ ] then 0 else -1
else if b == [ ] then
1
else if result == 0 then
lib.lists.compare compare (builtins.tail a) (builtins.tail b)
else
result;
## Get the last element of a list.
##
## @type List a -> a
last =
list:
assert lib.errors.trace (list != [ ]) "List cannot be empty";
builtins.elemAt list (builtins.length list - 1);
## Slice part of a list to create a new list.
##
## @type Int -> Int -> List -> List
slice =
start: count: list:
let
listLength = builtins.length list;
resultLength =
if start >= listLength then
0
else if start + count > listLength then
listLength - start
else
count;
in
builtins.genList (i: builtins.elemAt list (start + i)) resultLength;
## Take the first n elements of a list.
##
## @type Int -> List -> List
take = lib.lists.slice 0;
## Drop the first n elements of a list.
##
## @type Int -> List -> List
drop =
count: list:
let
listLength = builtins.length list;
in
lib.lists.slice count listLength list;
## Take all but the last element of a list.
##
## @type List -> List
init =
list:
assert lib.errors.trace (builtins.length list != 0) "lib.lists.init: list must not be empty.";
lib.lists.take (builtins.length list - 1) list;
## Reverse a list.
##
## @type List -> List
reverse =
list:
let
length = builtins.length list;
create = i: builtins.elemAt list (length - i - 1);
in
builtins.genList create length;
## Interleave a list with a separator.
##
## @type Separator -> List -> List
intersperse =
separator: list:
let
length = builtins.length list;
in
if length < 2 then
list
else
builtins.tail (
builtins.concatMap (part: [
separator
part
]) list
);
## Create a list of integers from a starting number to an ending
## number. This *includes* the ending number as well.
##
## @type Int -> Int -> List
range = start: end: if start > end then [ ] else builtins.genList (i: start + i) (end - start + 1);
## Depending on a given condition, either use the given value (as
## a list) or an empty list.
##
## @type Attrs a b => Bool -> a -> a | b
when =
condition: value: if condition then if builtins.isList value then value else [ value ] else [ ];
## Count the number of items in a list that satisfy a given predicate.
##
## @type (a -> Bool) -> List a -> Int
count =
predicate: list:
builtins.foldl' (total: value: if predicate value then total + 1 else total) 0 list;
## Remove duplicate items from a list.
##
## @type List -> List
unique =
list:
let
filter = result: value: if builtins.elem value result then result else result ++ [ value ];
in
builtins.foldl' filter [ ] list;
## Flatten a list of lists into a single list.
##
## @type List (List a) -> List a
flatten =
value: if builtins.isList value then builtins.concatMap lib.lists.flatten value else [ value ];
}

View file

@ -50,14 +50,32 @@ in
"computes the cartesian product of two lists" =
let
expected = [
{ x = 1; y = "a"; }
{ x = 1; y = "b"; }
{ x = 2; y = "a"; }
{ x = 2; y = "b"; }
{
x = 1;
y = "a";
}
{
x = 1;
y = "b";
}
{
x = 2;
y = "a";
}
{
x = 2;
y = "b";
}
];
actual = lib.lists.cartesian.product {
x = [ 1 2 ];
y = [ "a" "b" ];
x = [
1
2
];
y = [
"a"
"b"
];
};
in
actual == expected;
@ -73,12 +91,17 @@ in
"2-b"
];
actual = lib.lists.cartesian.map ({ x, y }: "${builtins.toString x}-${y}") {
x = [ 1 2 ];
y = [ "a" "b" ];
x = [
1
2
];
y = [
"a"
"b"
];
};
in
(builtins.trace (builtins.deepSeq actual actual))
actual == expected;
(builtins.trace (builtins.deepSeq actual actual)) actual == expected;
};
};
@ -189,7 +212,7 @@ in
actual = lib.lists.last [ ];
evaluated = builtins.tryEval actual;
in
!evaluated.success;
!evaluated.success;
};
"slice" = {

View file

@ -1,13 +1,11 @@
lib: {
math = {
## Return the smaller of two numbers.
##
## @type Int -> Int -> Int
min = x: y: if x < y then x else y;
## Return the smaller of two numbers.
##
## @type Int -> Int -> Int
min = x: y: if x < y then x else y;
## Return the larger of two numbers.
##
## @type Int -> Int -> Int
max = x: y: if x > y then x else y;
};
## Return the larger of two numbers.
##
## @type Int -> Int -> Int
max = x: y: if x > y then x else y;
}

File diff suppressed because it is too large Load diff

View file

@ -212,7 +212,7 @@ in
aux = lib.options.create {
description = "Hello world!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -222,7 +222,7 @@ in
aux = lib.options.create {
example = "Heya!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -230,8 +230,7 @@ in
];
};
in
evaluated.options.aux.description == "Hello world!"
&& evaluated.options.aux.example == "Heya!";
evaluated.options.aux.description == "Hello world!" && evaluated.options.aux.example == "Heya!";
"fails to merge unmergeable options" =
let
evaluated = lib.modules.run {
@ -241,7 +240,7 @@ in
aux = lib.options.create {
description = "Hello world!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -251,7 +250,7 @@ in
aux = lib.options.create {
description = "Heya!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -259,7 +258,7 @@ in
];
};
in
(builtins.tryEval evaluated.options.aux.description).success == false;
(builtins.tryEval evaluated.options.aux.description).success == false;
"merges three options" =
let
evaluated = lib.modules.run {
@ -285,7 +284,7 @@ in
aux = lib.options.create {
example = "Heya!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -293,9 +292,11 @@ in
{
options = {
aux = lib.options.create {
default = { hello = "world"; };
default = {
hello = "world";
};
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -303,9 +304,9 @@ in
];
};
in
evaluated.options.aux.description == "Hello world!"
&& evaluated.options.aux.example == "Heya!"
&& evaluated.options.aux.default == { hello = "world"; };
evaluated.options.aux.description == "Hello world!"
&& evaluated.options.aux.example == "Heya!"
&& evaluated.options.aux.default == { hello = "world"; };
"overrides option properties" =
let
evaluated = lib.modules.run {
@ -315,7 +316,7 @@ in
aux = lib.options.create {
description = lib.modules.overrides.force "Hello world!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -325,14 +326,15 @@ in
aux = lib.options.create {
description = "Heya!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
}
];
};
in evaluated.options.aux.description == "Hello world!";
in
evaluated.options.aux.description == "Hello world!";
"permits duplicate option properties" =
let
evaluated = lib.modules.run {
@ -342,7 +344,7 @@ in
aux = lib.options.create {
description = "Hello world!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -352,7 +354,7 @@ in
aux = lib.options.create {
description = "Hello world!";
type = lib.types.submodules.of {
modules = [];
modules = [ ];
};
};
};
@ -360,7 +362,7 @@ in
];
};
in
evaluated.options.aux.description == "Hello world!";
evaluated.options.aux.description == "Hello world!";
};
"normalize" = {

View file

@ -1,72 +1,70 @@
lib: {
numbers = {
into = {
## Convert a number into a string.
##
## @type Int | Float -> String
string = value: if builtins.isInt value then builtins.toString value else builtins.toJSON value;
## Convert a number into a list of digits in the given base.
##
## @type Int -> Int -> List Int
base =
base: target:
let
process =
value:
let
r = value - ((value / base) * base);
q = (value - r) / base;
in
if value < base then [ value ] else [ r ] ++ process q;
result = process target;
in
assert lib.errors.trace (builtins.isInt base) "Base must be an integer.";
assert lib.errors.trace (builtins.isInt target) "Target must be an integer.";
assert lib.errors.trace (base >= 2) "Base must be at least 2.";
assert lib.errors.trace (target >= 0) "Target cannot be negative.";
lib.lists.reverse result;
## Convert a number into a hexadecimal string.
##
## @type Int -> String
hex =
value:
let
serialize =
part:
if part < 10 then
builtins.toString part
else if part == 10 then
"A"
else if part == 11 then
"B"
else if part == 12 then
"C"
else if part == 13 then
"D"
else if part == 14 then
"E"
else if part == 15 then
"F"
else
builtins.throw "Invalid hex digit.";
in
lib.strings.concatMap serialize (lib.numbers.into.base 16 value);
};
## Compare two numbers. When the first number is less than the second, -1
## is returned. When the first number is greater than the second, 1 is
## returned. When the numbers are equal, 0 is returned.
into = {
## Convert a number into a string.
##
## @type Int -> Int -> Int
compare =
a: b:
if a < b then
-1
else if a > b then
1
else
0;
## @type Int | Float -> String
string = value: if builtins.isInt value then builtins.toString value else builtins.toJSON value;
## Convert a number into a list of digits in the given base.
##
## @type Int -> Int -> List Int
base =
base: target:
let
process =
value:
let
r = value - ((value / base) * base);
q = (value - r) / base;
in
if value < base then [ value ] else [ r ] ++ process q;
result = process target;
in
assert lib.errors.trace (builtins.isInt base) "Base must be an integer.";
assert lib.errors.trace (builtins.isInt target) "Target must be an integer.";
assert lib.errors.trace (base >= 2) "Base must be at least 2.";
assert lib.errors.trace (target >= 0) "Target cannot be negative.";
lib.lists.reverse result;
## Convert a number into a hexadecimal string.
##
## @type Int -> String
hex =
value:
let
serialize =
part:
if part < 10 then
builtins.toString part
else if part == 10 then
"A"
else if part == 11 then
"B"
else if part == 12 then
"C"
else if part == 13 then
"D"
else if part == 14 then
"E"
else if part == 15 then
"F"
else
builtins.throw "Invalid hex digit.";
in
lib.strings.concatMap serialize (lib.numbers.into.base 16 value);
};
## Compare two numbers. When the first number is less than the second, -1
## is returned. When the first number is greater than the second, 1 is
## returned. When the numbers are equal, 0 is returned.
##
## @type Int -> Int -> Int
compare =
a: b:
if a < b then
-1
else if a > b then
1
else
0;
}

View file

@ -1,360 +1,357 @@
lib: {
options = {
merge = {
## Merge a list of option definitions into a single value.
##
## @type Location -> List Definition -> Any
default =
location: definitions:
let
values = lib.options.getDefinitionValues definitions;
first = builtins.elemAt values 0;
mergedFunctions = x: lib.options.mergeDefault location (builtins.map (f: f x) values);
mergedLists = builtins.concatLists values;
mergedAttrs = builtins.foldl' lib.attrs.merge { } values;
mergedBools = builtins.any lib.bools.or false values;
mergedStrings = lib.strings.concat values;
in
if builtins.length values == 1 then
builtins.elemAt values 0
else if builtins.all builtins.isFunction values then
mergedFunctions
else if builtins.all builtins.isList values then
mergedLists
else if builtins.all builtins.isAttrs values then
mergedAttrs
else if builtins.all builtins.isBool values then
mergedBools
else if builtins.all lib.strings.isString values then
mergedStrings
else if builtins.all builtins.isInt values && builtins.all (x: x == first) values then
first
# TODO: Improve this error message to show the location and definitions for the option.
else
builtins.throw "Cannot merge definitions.";
## Merge multiple option definitions together.
##
## @type Location -> Type -> List Definition
definitions =
location: type: definitions:
let
identifier = lib.options.getIdentifier location;
resolve =
definition:
let
properties = builtins.addErrorContext "while evaluating definitions from `${definition.__file__ or "<unknown>"}`:" (
lib.modules.apply.properties definition.value
);
normalize = value: {
__file__ = definition.__file__ or "<unknown>";
inherit value;
};
in
builtins.map normalize properties;
resolved = builtins.concatMap resolve definitions;
overridden = lib.modules.apply.overrides resolved;
values =
if builtins.any (definition: lib.types.is "order" definition.value) overridden.values then
lib.modules.apply.order overridden.values
else
overridden.values;
isDefined = values != [ ];
invalid = builtins.filter (definition: !(type.check definition.value)) values;
merged =
if isDefined then
if builtins.all (definition: type.check definition.value) values then
type.merge location values
else
builtins.throw "A definition for `${identifier}` is not of type `${type.description}`. Definition values:${lib.options.getDefinitions invalid}"
else
builtins.throw "The option `${identifier}` is used but not defined.";
optional = if isDefined then { value = merged; } else { };
in
{
inherit
isDefined
values
merged
optional
;
raw = {
inherit values;
inherit (overridden) highestPriority;
};
};
## Merge multiple option declarations together.
##
## @type Location -> List Option
declarations =
location: options:
let
merge =
result: option:
let
mergedType = result.type.mergeType option.options.type.functor;
isTypeMergeable = mergedType != null;
shared = name: option.options ? ${name} && result ? ${name};
typeSet = lib.attrs.when ((shared "type") && isTypeMergeable) { type = mergedType; };
files = result.declarations;
serializedFiles = builtins.concatStringsSep " and " files;
typeSubmodules = option.options.type.submodules or null;
submodules =
if typeSubmodules != null then
builtins.map (module: {
__file__ = option.__file__;
includes = [ module ];
}) typeSubmodules
++ result.options
else
result.options;
getPriority =
value:
if lib.types.is "override" value then
value.priority
else
lib.modules.DEFAULT_PRIORITY;
higherPriority = value1: value2: (getPriority value1) < (getPriority value2);
mergeProperty = name:
if (higherPriority result.${name} option.options.${name}) then result.${name}
else if (higherPriority option.options.${name} result.${name}) then option.options.${name}
else if result.${name} == option.options.${name} then result.${name} # This is mostly so any defaults/etc. get merged cleanly
else builtins.throw "The option `${lib.options.getIdentifier location}` in `${option.__file__}` is already declared in ${serializedFiles}, but their ${name} values could not be merged. Please set one as a higher priority (e.g. using `${name} = lib.modules.overrides.force <value>`) or remove a definition";
in
if
shared "type" && !isTypeMergeable
then
builtins.throw "The option `${lib.options.getIdentifier location}` in `${option.__file__}` is already declared in ${serializedFiles} but its type is not mergeable. Please remove a definition"
else
option.options
// result
// {
declarations = result.declarations ++ [ option.__file__ ];
options = submodules;
}
// (if shared "default" then { default = mergeProperty "default"; } else {})
// (if shared "example" then { example = mergeProperty "example"; } else {})
// (if shared "description" then { description = mergeProperty "description"; } else {})
// (if shared "apply" then { apply = mergeProperty "apply"; } else {})
// typeSet;
in
builtins.foldl' merge {
inherit location;
declarations = [ ];
options = [ ];
} options;
## Merge an option, only supporting a single unique definition.
##
## @type String -> Location -> List Definition -> Any
unique =
message: location: definitions:
let
identifier = lib.options.getIdentifier location;
total = builtins.length definitions;
first = builtins.elemAt definitions 0;
in
if total == 1 then
first.value
else if total == 0 then
builtins.throw "Cannot merge unused option `${identifier}`.\n${message}"
else
builtins.throw "The option `${identifier}` is defined multiple times, but must be unique.\n${message}\nDefinitions:${lib.options.getDefinitions definitions}";
## Merge a single instance of an option.
##
## @type Location -> List Definition -> Any
one = lib.options.merge.unique "";
equal =
location: definitions:
let
identifier = lib.options.getIdentifier location;
first = builtins.elemAt definitions 0;
rest = builtins.tail definitions;
merge =
x: y:
if x != y then
builtins.throw "The option `${identifier}` has conflicting definitions:${lib.options.getDefinitions definitions}"
else
x;
merged = builtins.foldl' merge first rest;
in
if builtins.length definitions == 0 then
builtins.throw "Cannot merge unused option `${identifier}`."
else if builtins.length definitions == 1 then
first.value
else
merged.value;
};
## Check whether a value is an option.
merge = {
## Merge a list of option definitions into a single value.
##
## @type Attrs -> Bool
is = lib.types.is "option";
## Create an option.
##
## @type { type? :: String | Null, apply? :: (a -> b) | Null, default? :: { value :: a, text :: String }, example? :: String | Null, visible? :: Bool | Null, internal? :: Bool | Null, writable? :: Bool | Null, description? :: String | Null } -> Option a
create =
settings@{
type ? lib.types.unspecified,
apply ? lib.modules.overrides.default null,
default ? lib.modules.overrides.default { },
example ? lib.modules.overrides.default null,
visible ? null,
internal ? null,
writable ? null,
description ? lib.modules.overrides.default null,
}:
{
__type__ = "option";
inherit
type
apply
default
example
visible
internal
writable
description
;
};
## Create a sink option.
##
## @type @alias lib.options.create
sink =
settings:
## @type Location -> List Definition -> Any
default =
location: definitions:
let
defaults = {
internal = true;
visible = false;
default = false;
description = "A sink option for unused definitions";
type = lib.types.create {
name = "sink";
check = lib.fp.const true;
merge = lib.fp.const (lib.fp.const false);
};
apply = value: builtins.throw "Cannot read the value of a Sink option.";
};
values = lib.options.getDefinitionValues definitions;
first = builtins.elemAt values 0;
mergedFunctions = x: lib.options.mergeDefault location (builtins.map (f: f x) values);
mergedLists = builtins.concatLists values;
mergedAttrs = builtins.foldl' lib.attrs.merge { } values;
mergedBools = builtins.any lib.bools.or false values;
mergedStrings = lib.strings.concat values;
in
lib.options.create (defaults // settings);
if builtins.length values == 1 then
builtins.elemAt values 0
else if builtins.all builtins.isFunction values then
mergedFunctions
else if builtins.all builtins.isList values then
mergedLists
else if builtins.all builtins.isAttrs values then
mergedAttrs
else if builtins.all builtins.isBool values then
mergedBools
else if builtins.all lib.strings.isString values then
mergedStrings
else if builtins.all builtins.isInt values && builtins.all (x: x == first) values then
first
# TODO: Improve this error message to show the location and definitions for the option.
else
builtins.throw "Cannot merge definitions.";
## Get the definition values from a list of options definitions.
## Merge multiple option definitions together.
##
## @type List Definition -> Any
getDefinitionValues = definitions: builtins.map (definition: definition.value) definitions;
## Convert a list of option identifiers into a single identifier.
##
## @type List String -> String
getIdentifier =
location:
let
special = [
# lib.types.attrs.of (lib.types.submodule {})
"<name>"
# lib.types.list.of (submodule {})
"*"
# lib.types.function
"<function body>"
];
escape = part: if builtins.elem part special then part else lib.strings.escape.nix.identifier part;
in
lib.strings.concatMapSep "." escape location;
## Get a string message of the definitions for an option.
##
## @type List Definition -> String
getDefinitions =
definitions:
let
serialize =
definition:
let
valueWithRecursionLimit = lib.generators.withRecursion {
limit = 10;
throw = false;
} definition.value;
eval = builtins.tryEval (lib.generators.pretty { } valueWithRecursionLimit);
lines = lib.strings.split "\n" eval.value;
linesLength = builtins.length lines;
firstFiveLines = lib.lists.take 5 lines;
ellipsis = lib.lists.when (linesLength > 5) "...";
value = builtins.concatStringsSep "\n " (firstFiveLines ++ ellipsis);
result =
if !eval.success then
""
else if linesLength > 1 then
":\n " + value
else
": " + value;
in
"\n- In `${definition.__file__}`${result}";
in
lib.strings.concatMap serialize definitions;
## Run a set of definitions, calculating the resolved value and associated information.
##
## @type Location -> Option -> List Definition -> String & { value :: Any, highestPriority :: Int, isDefined :: Bool, files :: List String, definitions :: List Any, definitionsWithLocations :: List Definition }
run =
location: option: definitions:
## @type Location -> Type -> List Definition
definitions =
location: type: definitions:
let
identifier = lib.options.getIdentifier location;
resolve =
definition:
let
properties = builtins.addErrorContext "while evaluating definitions from `${definition.__file__ or "<unknown>"}`:" (
lib.modules.apply.properties definition.value
);
normalize = value: {
__file__ = definition.__file__ or "<unknown>";
inherit value;
};
in
builtins.map normalize properties;
definitionsWithDefault =
if option ? default && option.default ? value then
[
{
__file__ = builtins.head option.declarations;
value = lib.modules.overrides.option option.default.value;
}
]
++ definitions
resolved = builtins.concatMap resolve definitions;
overridden = lib.modules.apply.overrides resolved;
values =
if builtins.any (definition: lib.types.is "order" definition.value) overridden.values then
lib.modules.apply.order overridden.values
else
definitions;
overridden.values;
isDefined = values != [ ];
invalid = builtins.filter (definition: !(type.check definition.value)) values;
merged =
if option.writable or null == false && builtins.length definitionsWithDefault > 1 then
let
separatedDefinitions = builtins.map (
definition:
definition
// {
value = (lib.options.merge.definitions location option.type [ definition ]).merged;
}
) definitionsWithDefault;
in
builtins.throw "The option `${identifier}` is not writable, but is set more than once:${lib.options.getDefinitions separatedDefinitions}"
if isDefined then
if builtins.all (definition: type.check definition.value) values then
type.merge location values
else
builtins.throw "A definition for `${identifier}` is not of type `${type.description}`. Definition values:${lib.options.getDefinitions invalid}"
else
lib.options.merge.definitions location option.type definitionsWithDefault;
builtins.throw "The option `${identifier}` is used but not defined.";
value = if option.apply or null != null then option.apply merged.merged else merged.merged;
optional = if isDefined then { value = merged; } else { };
in
option
// {
value = builtins.addErrorContext "while evaluating the option `${identifier}`:" value;
highestPriority = merged.raw.highestPriority;
isDefined = merged.isDefined;
files = builtins.map (definition: definition.__file__) merged.values;
definitions = lib.options.getDefinitionValues merged.values;
definitionsWithLocations = merged.values;
__toString = identifier;
{
inherit
isDefined
values
merged
optional
;
raw = {
inherit values;
inherit (overridden) highestPriority;
};
};
## Merge multiple option declarations together.
##
## @type Location -> List Option
declarations =
location: options:
let
merge =
result: option:
let
mergedType = result.type.mergeType option.options.type.functor;
isTypeMergeable = mergedType != null;
shared = name: option.options ? ${name} && result ? ${name};
typeSet = lib.attrs.when ((shared "type") && isTypeMergeable) { type = mergedType; };
files = result.declarations;
serializedFiles = builtins.concatStringsSep " and " files;
typeSubmodules = option.options.type.submodules or null;
submodules =
if typeSubmodules != null then
builtins.map (module: {
__file__ = option.__file__;
includes = [ module ];
}) typeSubmodules
++ result.options
else
result.options;
getPriority =
value: if lib.types.is "override" value then value.priority else lib.modules.DEFAULT_PRIORITY;
higherPriority = value1: value2: (getPriority value1) < (getPriority value2);
mergeProperty =
name:
if (higherPriority result.${name} option.options.${name}) then
result.${name}
else if (higherPriority option.options.${name} result.${name}) then
option.options.${name}
else if result.${name} == option.options.${name} then
result.${name} # This is mostly so any defaults/etc. get merged cleanly
else
builtins.throw "The option `${lib.options.getIdentifier location}` in `${option.__file__}` is already declared in ${serializedFiles}, but their ${name} values could not be merged. Please set one as a higher priority (e.g. using `${name} = lib.modules.overrides.force <value>`) or remove a definition";
in
if shared "type" && !isTypeMergeable then
builtins.throw "The option `${lib.options.getIdentifier location}` in `${option.__file__}` is already declared in ${serializedFiles} but its type is not mergeable. Please remove a definition"
else
option.options
// result
// {
declarations = result.declarations ++ [ option.__file__ ];
options = submodules;
}
// (if shared "default" then { default = mergeProperty "default"; } else { })
// (if shared "example" then { example = mergeProperty "example"; } else { })
// (if shared "description" then { description = mergeProperty "description"; } else { })
// (if shared "apply" then { apply = mergeProperty "apply"; } else { })
// typeSet;
in
builtins.foldl' merge {
inherit location;
declarations = [ ];
options = [ ];
} options;
## Merge an option, only supporting a single unique definition.
##
## @type String -> Location -> List Definition -> Any
unique =
message: location: definitions:
let
identifier = lib.options.getIdentifier location;
total = builtins.length definitions;
first = builtins.elemAt definitions 0;
in
if total == 1 then
first.value
else if total == 0 then
builtins.throw "Cannot merge unused option `${identifier}`.\n${message}"
else
builtins.throw "The option `${identifier}` is defined multiple times, but must be unique.\n${message}\nDefinitions:${lib.options.getDefinitions definitions}";
## Merge a single instance of an option.
##
## @type Location -> List Definition -> Any
one = lib.options.merge.unique "";
equal =
location: definitions:
let
identifier = lib.options.getIdentifier location;
first = builtins.elemAt definitions 0;
rest = builtins.tail definitions;
merge =
x: y:
if x != y then
builtins.throw "The option `${identifier}` has conflicting definitions:${lib.options.getDefinitions definitions}"
else
x;
merged = builtins.foldl' merge first rest;
in
if builtins.length definitions == 0 then
builtins.throw "Cannot merge unused option `${identifier}`."
else if builtins.length definitions == 1 then
first.value
else
merged.value;
};
## Check whether a value is an option.
##
## @type Attrs -> Bool
is = lib.types.is "option";
## Create an option.
##
## @type { type? :: String | Null, apply? :: (a -> b) | Null, default? :: { value :: a, text :: String }, example? :: String | Null, visible? :: Bool | Null, internal? :: Bool | Null, writable? :: Bool | Null, description? :: String | Null } -> Option a
create =
settings@{
type ? lib.types.unspecified,
apply ? lib.modules.overrides.default null,
default ? lib.modules.overrides.default { },
example ? lib.modules.overrides.default null,
visible ? null,
internal ? null,
writable ? null,
description ? lib.modules.overrides.default null,
}:
{
__type__ = "option";
inherit
type
apply
default
example
visible
internal
writable
description
;
};
## Create a sink option.
##
## @type @alias lib.options.create
sink =
settings:
let
defaults = {
internal = true;
visible = false;
default = false;
description = "A sink option for unused definitions";
type = lib.types.create {
name = "sink";
check = lib.fp.const true;
merge = lib.fp.const (lib.fp.const false);
};
apply = value: builtins.throw "Cannot read the value of a Sink option.";
};
in
lib.options.create (defaults // settings);
## Get the definition values from a list of options definitions.
##
## @type List Definition -> Any
getDefinitionValues = definitions: builtins.map (definition: definition.value) definitions;
## Convert a list of option identifiers into a single identifier.
##
## @type List String -> String
getIdentifier =
location:
let
special = [
# lib.types.attrs.of (lib.types.submodule {})
"<name>"
# lib.types.list.of (submodule {})
"*"
# lib.types.function
"<function body>"
];
escape = part: if builtins.elem part special then part else lib.strings.escape.nix.identifier part;
in
lib.strings.concatMapSep "." escape location;
## Get a string message of the definitions for an option.
##
## @type List Definition -> String
getDefinitions =
definitions:
let
serialize =
definition:
let
valueWithRecursionLimit = lib.generators.withRecursion {
limit = 10;
throw = false;
} definition.value;
eval = builtins.tryEval (lib.generators.pretty { } valueWithRecursionLimit);
lines = lib.strings.split "\n" eval.value;
linesLength = builtins.length lines;
firstFiveLines = lib.lists.take 5 lines;
ellipsis = lib.lists.when (linesLength > 5) "...";
value = builtins.concatStringsSep "\n " (firstFiveLines ++ ellipsis);
result =
if !eval.success then
""
else if linesLength > 1 then
":\n " + value
else
": " + value;
in
"\n- In `${definition.__file__}`${result}";
in
lib.strings.concatMap serialize definitions;
## Run a set of definitions, calculating the resolved value and associated information.
##
## @type Location -> Option -> List Definition -> String & { value :: Any, highestPriority :: Int, isDefined :: Bool, files :: List String, definitions :: List Any, definitionsWithLocations :: List Definition }
run =
location: option: definitions:
let
identifier = lib.options.getIdentifier location;
definitionsWithDefault =
if option ? default && option.default ? value then
[
{
__file__ = builtins.head option.declarations;
value = lib.modules.overrides.option option.default.value;
}
]
++ definitions
else
definitions;
merged =
if option.writable or null == false && builtins.length definitionsWithDefault > 1 then
let
separatedDefinitions = builtins.map (
definition:
definition
// {
value = (lib.options.merge.definitions location option.type [ definition ]).merged;
}
) definitionsWithDefault;
in
builtins.throw "The option `${identifier}` is not writable, but is set more than once:${lib.options.getDefinitions separatedDefinitions}"
else
lib.options.merge.definitions location option.type definitionsWithDefault;
value = if option.apply or null != null then option.apply merged.merged else merged.merged;
in
option
// {
value = builtins.addErrorContext "while evaluating the option `${identifier}`:" value;
highestPriority = merged.raw.highestPriority;
isDefined = merged.isDefined;
files = builtins.map (definition: definition.__file__) merged.values;
definitions = lib.options.getDefinitionValues merged.values;
definitionsWithLocations = merged.values;
__toString = identifier;
};
}

View file

@ -1,52 +1,50 @@
lib: {
packages = {
## Check whether a value is a derivation. Note that this will also return true
## for "fake" derivations which are constructed by helpers such as
## `lib.paths.into.drv` for convenience.
##
## @type a -> Bool
isDerivation = value: value.type or null == "derivation";
## Check whether a value is a derivation. Note that this will also return true
## for "fake" derivations which are constructed by helpers such as
## `lib.paths.into.drv` for convenience.
##
## @type a -> Bool
isDerivation = value: value.type or null == "derivation";
## Sanitize a string to produce a valid name for a derivation.
##
## @type String -> String
sanitizeDerivationName =
let
validate = builtins.match "[[:alnum:]+_?=-][[:alnum:]+._?=-]*";
in
value:
# First detect the common case of already valid strings, to speed those up
if builtins.stringLength value <= 207 && validate value != null then
builtins.unsafeDiscardStringContext value
else
lib.fp.pipe [
# Get rid of string context. This is safe under the assumption that the
# resulting string is only used as a derivation name
builtins.unsafeDiscardStringContext
## Sanitize a string to produce a valid name for a derivation.
##
## @type String -> String
sanitizeDerivationName =
let
validate = builtins.match "[[:alnum:]+_?=-][[:alnum:]+._?=-]*";
in
value:
# First detect the common case of already valid strings, to speed those up
if builtins.stringLength value <= 207 && validate value != null then
builtins.unsafeDiscardStringContext value
else
lib.fp.pipe [
# Get rid of string context. This is safe under the assumption that the
# resulting string is only used as a derivation name
builtins.unsafeDiscardStringContext
# Strip all leading "."
(x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0)
# Strip all leading "."
(x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0)
(lib.strings.split "[^[:alnum:]+._?=-]+")
(lib.strings.split "[^[:alnum:]+._?=-]+")
# Replace invalid character ranges with a "-"
(lib.strings.concatMap (x: if builtins.isList x then "-" else x))
# Replace invalid character ranges with a "-"
(lib.strings.concatMap (x: if builtins.isList x then "-" else x))
# Limit to 211 characters (minus 4 chars for ".drv")
(x: builtins.substring (lib.math.max (builtins.stringLength x - 207) 0) (-1) x)
# Limit to 211 characters (minus 4 chars for ".drv")
(x: builtins.substring (lib.math.max (builtins.stringLength x - 207) 0) (-1) x)
# If the result is empty, replace it with "unknown"
(x: if builtins.stringLength x == 0 then "unknown" else x)
] value;
# If the result is empty, replace it with "unknown"
(x: if builtins.stringLength x == 0 then "unknown" else x)
] value;
## Get an output of a derivation.
##
## @type String -> Derivation -> String
getOutput =
output: package:
if !package ? outputSpecified || !package.outputSpecified then
package.${output} or package.out or package
else
package;
};
## Get an output of a derivation.
##
## @type String -> Derivation -> String
getOutput =
output: package:
if !package ? outputSpecified || !package.outputSpecified then
package.${output} or package.out or package
else
package;
}

View file

@ -1,67 +1,65 @@
lib: {
paths = {
into = {
## Convert a path into a derivation.
##
## @type Path -> Derivation
drv =
value:
let
path = builtins.storePath value;
result = {
type = "derivation";
name = lib.packages.sanitizeDerivationName (builtins.substring 33 (-1) (builtins.baseNameOf path));
outPath = path;
outputs = [ "out" ];
outputName = "out";
out = result;
};
in
result;
};
validate = {
## Check whether a path is contained within the Nix store.
##
## @type Path -> Bool
store =
value:
if lib.strings.validate.stringifiable value then
builtins.substring 0 1 (builtins.toString value) == "/"
&& builtins.dirOf (builtins.toString value) == builtins.storeDir
else
false;
};
## Create a search path from a list of paths.
into = {
## Convert a path into a derivation.
##
## @type String -> [String] -> String
search =
target: paths:
lib.strings.concatMapSep ":" (path: path + "/" + target) (
builtins.filter (value: value != null) paths
);
## Create a search path from a list of packages.
##
## @type String -> [Derivation] -> String
searchFromOutput =
output: target: packages:
lib.paths.search target (builtins.map (lib.packages.getOutput output) packages);
## Create a search path for the binary output of a package.
##
## @type [Derivation] -> String
bin = lib.paths.searchFromOutput "bin" "bin";
## Create a search path for the library output of a package.
##
## @type [Derivation] -> String
lib = lib.paths.searchFromOutput "lib" "lib";
## Create a search path for the include output of a package.
##
## @type [Derivation] -> String
include = lib.paths.searchFromOutput "dev" "include";
## @type Path -> Derivation
drv =
value:
let
path = builtins.storePath value;
result = {
type = "derivation";
name = lib.packages.sanitizeDerivationName (builtins.substring 33 (-1) (builtins.baseNameOf path));
outPath = path;
outputs = [ "out" ];
outputName = "out";
out = result;
};
in
result;
};
validate = {
## Check whether a path is contained within the Nix store.
##
## @type Path -> Bool
store =
value:
if lib.strings.validate.stringifiable value then
builtins.substring 0 1 (builtins.toString value) == "/"
&& builtins.dirOf (builtins.toString value) == builtins.storeDir
else
false;
};
## Create a search path from a list of paths.
##
## @type String -> [String] -> String
search =
target: paths:
lib.strings.concatMapSep ":" (path: path + "/" + target) (
builtins.filter (value: value != null) paths
);
## Create a search path from a list of packages.
##
## @type String -> [Derivation] -> String
searchFromOutput =
output: target: packages:
lib.paths.search target (builtins.map (lib.packages.getOutput output) packages);
## Create a search path for the binary output of a package.
##
## @type [Derivation] -> String
bin = lib.paths.searchFromOutput "bin" "bin";
## Create a search path for the library output of a package.
##
## @type [Derivation] -> String
lib = lib.paths.searchFromOutput "lib" "lib";
## Create a search path for the include output of a package.
##
## @type [Derivation] -> String
include = lib.paths.searchFromOutput "dev" "include";
}

View file

@ -1,54 +1,52 @@
lib: {
points = {
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows
## us to define recursive functions without worrying about the order of their
## definitions.
##
## @type (a -> a) -> a
fix =
f:
let
x = f x;
in
x;
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows
## us to define recursive functions without worrying about the order of their
## definitions.
##
## @type (a -> a) -> a
fix =
f:
let
x = f x;
in
x;
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows
## us to define recursive functions without worrying about the order of their
## definitions. Unlike `fix`, the resulting value is also given a `__unfix__`
## attribute that is set to the original function passed to `fix'`.
##
## @type (a -> a) -> a & { __unfix__ :: (a -> a) }
fix' =
f:
let
x = f x // {
__unfix__ = f;
};
in
x;
## Calculate the fixed point of a function. This will evaluate the function `f`
## until its result settles (or Nix's recursion limit is reached). This allows
## us to define recursive functions without worrying about the order of their
## definitions. Unlike `fix`, the resulting value is also given a `__unfix__`
## attribute that is set to the original function passed to `fix'`.
##
## @type (a -> a) -> a & { __unfix__ :: (a -> a) }
fix' =
f:
let
x = f x // {
__unfix__ = f;
};
in
x;
## Extend a function's output with an additional function. This is the basis for
## features like overlays.
##
## @type (a -> b -> c) -> (a -> b) -> a -> b & c
extends =
g: f: self:
let
previous = f self;
next = g self previous;
in
previous // next;
## Extend a function's output with an additional function. This is the basis for
## features like overlays.
##
## @type (a -> b -> c) -> (a -> b) -> a -> b & c
extends =
g: f: self:
let
previous = f self;
next = g self previous;
in
previous // next;
## Add an `extend` method to the result of a function.
##
## @type (a -> b) -> b & { extend :: (a -> b -> c) -> b & c }
withExtend =
f:
let
create = self: (f self) // { extend = g: lib.points.withExtend (lib.points.extends g f); };
in
lib.points.fix' create;
};
## Add an `extend` method to the result of a function.
##
## @type (a -> b) -> b & { extend :: (a -> b -> c) -> b & c }
withExtend =
f:
let
create = self: (f self) // { extend = g: lib.points.withExtend (lib.points.extends g f); };
in
lib.points.fix' create;
}

View file

@ -1,342 +1,340 @@
lib: {
strings = {
into = {
## Convert a character to an integer.
##
## @type String -> Integer
int = char: builtins.getAttr char lib.strings.ascii;
into = {
## Convert a character to an integer.
##
## @type String -> Integer
int = char: builtins.getAttr char lib.strings.ascii;
## Convert a string into a list of characters.
## Convert a string into a list of characters.
##
## @type String -> List String
chars =
value:
let
range = lib.lists.range 0 (builtins.stringLength value - 1);
pick = index: builtins.substring index 1 value;
in
builtins.map pick range;
shell = {
## Convert a value into a shell variable.
##
## @type String -> List String
chars =
value:
## @type String -> Any -> String
var =
name: target:
let
range = lib.lists.range 0 (builtins.stringLength value - 1);
pick = index: builtins.substring index 1 value;
baseVar = "${name}=${lib.strings.escape.shell.arg target}";
listVar = "declare -a ${name}=(${lib.strings.escape.shell.args target})";
attrsVar = "declare -A ${name}=(${
builtins.concatStringsSep " " (
lib.attrs.mapToList (
k: v: "[${lib.strings.escape.shell.arg k}]=${lib.strings.escape.shell.arg v}"
) target
)
})";
in
builtins.map pick range;
assert lib.errors.trace (lib.strings.validate.posix name) "Invalid shell variable name: ${name}";
if builtins.isAttrs target && !lib.strings.validate.stringifiable target then
attrsVar
else if builtins.isList target then
listVar
else
baseVar;
shell = {
## Convert a value into a shell variable.
##
## @type String -> Any -> String
var =
name: target:
## Create shell variables for a map of values.
##
## @type Attrs -> String
vars =
target: builtins.concatStringsSep "\n" (lib.attrs.mapToList lib.strings.into.shell.var target);
};
};
escape = {
## Escape parts of a string.
##
## @type List String -> String -> String
any =
patterns: source:
let
escaped = builtins.map (x: "\\${x}") patterns;
replacer = builtins.replaceStrings patterns escaped;
in
replacer source;
## Escape a given set of characters in a string using their
## ASCII code prefixed with "\x".
##
## @type List String -> String
c =
list:
let
serialize =
char:
let
baseVar = "${name}=${lib.strings.escape.shell.arg target}";
listVar = "declare -a ${name}=(${lib.strings.escape.shell.args target})";
attrsVar = "declare -A ${name}=(${
builtins.concatStringsSep " " (
lib.attrs.mapToList (
k: v: "[${lib.strings.escape.shell.arg k}]=${lib.strings.escape.shell.arg v}"
) target
)
})";
hex = lib.numbers.into.hex (lib.strings.into.int char);
in
assert lib.errors.trace (lib.strings.validate.posix name) "Invalid shell variable name: ${name}";
if builtins.isAttrs target && !lib.strings.validate.stringifiable target then
attrsVar
else if builtins.isList target then
listVar
else
baseVar;
"\\x${lib.strings.lower hex}";
in
builtins.replaceStrings list (builtins.map serialize list);
## Create shell variables for a map of values.
##
## @type Attrs -> String
vars =
target: builtins.concatStringsSep "\n" (lib.attrs.mapToList lib.strings.into.shell.var target);
};
nix = {
## Escape a string of Nix code.
##
## @type String -> String
value = value: lib.strings.escape.any [ "$" ] (builtins.toJSON value);
## Escape a string for use as a Nix identifier.
##
## @type String -> String
identifier =
value:
if builtins.match "[a-zA-Z_][a-zA-Z0-9_'-]*" value != null then
value
else
lib.strings.escape.nix.value value;
};
escape = {
## Escape parts of a string.
##
## @type List String -> String -> String
any =
patterns: source:
let
escaped = builtins.map (x: "\\${x}") patterns;
replacer = builtins.replaceStrings patterns escaped;
in
replacer source;
## Escape a string for use in a regular expression.
##
## @type String -> String
regex = lib.strings.escape.any (lib.strings.into.chars "\\[{()^$?*+|.");
## Escape a given set of characters in a string using their
## ASCII code prefixed with "\x".
## Escape a string for use in XML.
##
## @type String -> String
xml =
builtins.replaceStrings
[
"\""
"'"
"<"
">"
"&"
]
[
"&quot;"
"&apos;"
"&lt;"
"&gt;"
"&amp;"
];
shell = {
## Escape a string for use as a shell argument.
##
## @type String -> String
arg = value: "'${builtins.replaceStrings [ "'" ] [ "'\\''" ] (builtins.toString value)}'";
## Escape multiple strings for use as shell arguments.
##
## @type List String -> String
c =
list:
let
serialize =
char:
let
hex = lib.numbers.into.hex (lib.strings.into.int char);
in
"\\x${lib.strings.lower hex}";
in
builtins.replaceStrings list (builtins.map serialize list);
nix = {
## Escape a string of Nix code.
##
## @type String -> String
value = value: lib.strings.escape.any [ "$" ] (builtins.toJSON value);
## Escape a string for use as a Nix identifier.
##
## @type String -> String
identifier =
value:
if builtins.match "[a-zA-Z_][a-zA-Z0-9_'-]*" value != null then
value
else
lib.strings.escape.nix.value value;
};
## Escape a string for use in a regular expression.
##
## @type String -> String
regex = lib.strings.escape.any (lib.strings.into.chars "\\[{()^$?*+|.");
## Escape a string for use in XML.
##
## @type String -> String
xml =
builtins.replaceStrings
[
"\""
"'"
"<"
">"
"&"
]
[
"&quot;"
"&apos;"
"&lt;"
"&gt;"
"&amp;"
];
shell = {
## Escape a string for use as a shell argument.
##
## @type String -> String
arg = value: "'${builtins.replaceStrings [ "'" ] [ "'\\''" ] (builtins.toString value)}'";
## Escape multiple strings for use as shell arguments.
##
## @type List String -> String
args = lib.strings.concatMapSep " " lib.strings.escape.shell.arg;
};
args = lib.strings.concatMapSep " " lib.strings.escape.shell.arg;
};
validate = {
## Check if a string is a valid POSIX identifier.
##
## @type String -> Bool
posix = name: builtins.match "[a-zA-Z_][a-zA-Z0-9_]*" name != null;
## Check if a value can be used as a string.
##
## @type Any -> Bool
stringifiable =
value: builtins.isString value || builtins.isPath value || value ? outPath || value ? __toString;
## Check whether a string is empty. This includes strings that
## only contain whitespace.
##
## @type String -> Bool
empty = value: builtins.match "[ \t\n]*" value != null;
};
order = {
## Create an ordered string that will be placed anywhere.
##
## @type String -> OrderedString
anywhere = value: {
inherit value;
deps = [ ];
};
## Create an ordered string that will be placed before its dependencies.
##
## @type String -> List String -> OrderedString
after = deps: value: { inherit deps value; };
## Apply the order for a list of ordered strings. This function also supports
## plain strings in the list so long as they have an accompanying static definition
## provided.
##
## @type Attrs -> List (OrderedString | String)
apply =
static: items:
let
process =
complete: remaining:
let
next = builtins.head remaining;
before = process complete next.deps;
after = process before.complete (builtins.tail remaining);
in
if builtins.length remaining == 0 then
{
inherit complete;
result = [ ];
}
else if builtins.isAttrs next then
{
inherit complete;
result = before.result ++ [ next.value ] ++ after.result;
}
else
process (complete // { "${next}" = true; }) ([ static.${next} ] ++ builtins.tail remaining);
processed = process { } items;
in
processed.result;
};
## Return a given string if a condition is true, otherwise return
## an empty string.
##
## @type Bool -> String -> String
when = condition: value: if condition then value else "";
## A table of ASCII characters mapped to their integer character code.
##
## @type Attrs
ascii = import ./ascii.nix;
## Lists of both upper and lower case ASCII characters.
##
## @type { upper :: List String, lower :: List String }
alphabet = import ./alphabet.nix;
## Concatenate a list of strings together.
##
## @type List String -> String
concat = builtins.concatStringsSep "";
## Concatenate and map a list of strings together.
##
## @type List String -> String
concatMap = lib.strings.concatMapSep "";
## Concatenate and map a list of strings together with a separator.
##
## @type String -> (a -> String) -> List a -> String
concatMapSep =
separator: f: list:
builtins.concatStringsSep separator (builtins.map f list);
## Change a string to uppercase.
##
## @type String -> String
upper = builtins.replaceStrings lib.strings.alphabet.lower lib.strings.alphabet.upper;
## Change a string to lowercase.
##
## @type String -> String
lower = builtins.replaceStrings lib.strings.alphabet.upper lib.strings.alphabet.lower;
## Add the context of one string to another.
##
## @type String -> String -> String
withContext = context: value: builtins.substring 0 0 context + value;
## Split a string by a separator.
##
## @type String -> String -> List String
split =
separator: value:
let
escaped = lib.strings.escape.regex (builtins.toString separator);
raw = builtins.split escaped (builtins.toString value);
parts = builtins.filter builtins.isString raw;
in
builtins.map (lib.strings.withContext value) parts;
## Check if a string starts with a given prefix.
##
## @type String -> String -> Bool
hasPrefix =
prefix: value:
let
text = builtins.substring 0 (builtins.stringLength prefix) value;
in
text == prefix;
## Check if a string ends with a given suffix.
##
## @type String -> String -> Bool
hasSuffix =
suffix: value:
let
valueLength = builtins.stringLength value;
suffixLength = builtins.stringLength suffix;
text = builtins.substring (valueLength - suffixLength) valueLength value;
in
(valueLength >= suffixLength) && text == suffix;
## Check if a string contains a given infix.
##
## @type String -> String -> Bool
hasInfix = infix: value: builtins.match ".*${lib.strings.escape.regex infix}.*" "${value}" != null;
## Remove a prefix from a string if it exists.
##
## @type String -> String -> String
removePrefix =
prefix: value:
let
prefixLength = builtins.stringLength prefix;
valueLength = builtins.stringLength value;
in
if lib.strings.hasPrefix prefix value then
builtins.substring prefixLength (valueLength - prefixLength) value
else
value;
## Remove a suffix from a string if it exists.
##
## @type String -> String -> String
removeSuffix =
suffix: value:
let
suffixLength = builtins.stringLength suffix;
valueLength = builtins.stringLength value;
in
if lib.strings.hasSuffix suffix value then
builtins.substring 0 (valueLength - suffixLength) value
else
value;
## Pad the start of a string with a character until it reaches
## a given length.
##
## @type Integer -> String -> String
padStart =
length: char: value:
let
valueLength = builtins.stringLength value;
padding = builtins.genList (_: char) (length - valueLength);
in
if valueLength < length then (builtins.concatStringsSep "" padding) + value else value;
## Pad the end of a string with a character until it reaches
## a given length.
##
## @type Integer -> String -> String
padEnd =
length: char: value:
let
valueLength = builtins.stringLength value;
padding = builtins.genList (_: char) (length - valueLength);
in
if valueLength < length then value + (builtins.concatStringsSep "" padding) else value;
};
validate = {
## Check if a string is a valid POSIX identifier.
##
## @type String -> Bool
posix = name: builtins.match "[a-zA-Z_][a-zA-Z0-9_]*" name != null;
## Check if a value can be used as a string.
##
## @type Any -> Bool
stringifiable =
value: builtins.isString value || builtins.isPath value || value ? outPath || value ? __toString;
## Check whether a string is empty. This includes strings that
## only contain whitespace.
##
## @type String -> Bool
empty = value: builtins.match "[ \t\n]*" value != null;
};
order = {
## Create an ordered string that will be placed anywhere.
##
## @type String -> OrderedString
anywhere = value: {
inherit value;
deps = [ ];
};
## Create an ordered string that will be placed before its dependencies.
##
## @type String -> List String -> OrderedString
after = deps: value: { inherit deps value; };
## Apply the order for a list of ordered strings. This function also supports
## plain strings in the list so long as they have an accompanying static definition
## provided.
##
## @type Attrs -> List (OrderedString | String)
apply =
static: items:
let
process =
complete: remaining:
let
next = builtins.head remaining;
before = process complete next.deps;
after = process before.complete (builtins.tail remaining);
in
if builtins.length remaining == 0 then
{
inherit complete;
result = [ ];
}
else if builtins.isAttrs next then
{
inherit complete;
result = before.result ++ [ next.value ] ++ after.result;
}
else
process (complete // { "${next}" = true; }) ([ static.${next} ] ++ builtins.tail remaining);
processed = process { } items;
in
processed.result;
};
## Return a given string if a condition is true, otherwise return
## an empty string.
##
## @type Bool -> String -> String
when = condition: value: if condition then value else "";
## A table of ASCII characters mapped to their integer character code.
##
## @type Attrs
ascii = import ./ascii.nix;
## Lists of both upper and lower case ASCII characters.
##
## @type { upper :: List String, lower :: List String }
alphabet = import ./alphabet.nix;
## Concatenate a list of strings together.
##
## @type List String -> String
concat = builtins.concatStringsSep "";
## Concatenate and map a list of strings together.
##
## @type List String -> String
concatMap = lib.strings.concatMapSep "";
## Concatenate and map a list of strings together with a separator.
##
## @type String -> (a -> String) -> List a -> String
concatMapSep =
separator: f: list:
builtins.concatStringsSep separator (builtins.map f list);
## Change a string to uppercase.
##
## @type String -> String
upper = builtins.replaceStrings lib.strings.alphabet.lower lib.strings.alphabet.upper;
## Change a string to lowercase.
##
## @type String -> String
lower = builtins.replaceStrings lib.strings.alphabet.upper lib.strings.alphabet.lower;
## Add the context of one string to another.
##
## @type String -> String -> String
withContext = context: value: builtins.substring 0 0 context + value;
## Split a string by a separator.
##
## @type String -> String -> List String
split =
separator: value:
let
escaped = lib.strings.escape.regex (builtins.toString separator);
raw = builtins.split escaped (builtins.toString value);
parts = builtins.filter builtins.isString raw;
in
builtins.map (lib.strings.withContext value) parts;
## Check if a string starts with a given prefix.
##
## @type String -> String -> Bool
hasPrefix =
prefix: value:
let
text = builtins.substring 0 (builtins.stringLength prefix) value;
in
text == prefix;
## Check if a string ends with a given suffix.
##
## @type String -> String -> Bool
hasSuffix =
suffix: value:
let
valueLength = builtins.stringLength value;
suffixLength = builtins.stringLength suffix;
text = builtins.substring (valueLength - suffixLength) valueLength value;
in
(valueLength >= suffixLength) && text == suffix;
## Check if a string contains a given infix.
##
## @type String -> String -> Bool
hasInfix = infix: value: builtins.match ".*${lib.strings.escape.regex infix}.*" "${value}" != null;
## Remove a prefix from a string if it exists.
##
## @type String -> String -> String
removePrefix =
prefix: value:
let
prefixLength = builtins.stringLength prefix;
valueLength = builtins.stringLength value;
in
if lib.strings.hasPrefix prefix value then
builtins.substring prefixLength (valueLength - prefixLength) value
else
value;
## Remove a suffix from a string if it exists.
##
## @type String -> String -> String
removeSuffix =
suffix: value:
let
suffixLength = builtins.stringLength suffix;
valueLength = builtins.stringLength value;
in
if lib.strings.hasSuffix suffix value then
builtins.substring 0 (valueLength - suffixLength) value
else
value;
## Pad the start of a string with a character until it reaches
## a given length.
##
## @type Integer -> String -> String
padStart =
length: char: value:
let
valueLength = builtins.stringLength value;
padding = builtins.genList (_: char) (length - valueLength);
in
if valueLength < length then (builtins.concatStringsSep "" padding) + value else value;
## Pad the end of a string with a character until it reaches
## a given length.
##
## @type Integer -> String -> String
padEnd =
length: char: value:
let
valueLength = builtins.stringLength value;
padding = builtins.genList (_: char) (length - valueLength);
in
if valueLength < length then value + (builtins.concatStringsSep "" padding) else value;
}

File diff suppressed because it is too large Load diff

View file

@ -1,58 +1,56 @@
lib: {
versions = {
## Check if a version is greater than another.
##
## @type String -> String -> Bool
gt = first: second: builtins.compareVersions first second == -1;
## Check if a version is greater than another.
##
## @type String -> String -> Bool
gt = first: second: builtins.compareVersions first second == -1;
## Check if a version is greater than or equal to another.
##
## @type String -> String -> Bool
gte = first: second: builtins.compareVersions first second != 1;
## Check if a version is greater than or equal to another.
##
## @type String -> String -> Bool
gte = first: second: builtins.compareVersions first second != 1;
## Check if a version is less than another.
##
## @type String -> String -> Bool
lt = first: second: builtins.compareVersions first second == 1;
## Check if a version is less than another.
##
## @type String -> String -> Bool
lt = first: second: builtins.compareVersions first second == 1;
## Check if a version is less than or equal to another.
##
## @type String -> String -> Bool
lte = first: second: builtins.compareVersions first second != -1;
## Check if a version is less than or equal to another.
##
## @type String -> String -> Bool
lte = first: second: builtins.compareVersions first second != -1;
## Check if a version is equal to another.
##
## @type String -> String -> Bool
eq = first: second: builtins.compareVersions first second == 0;
## Check if a version is equal to another.
##
## @type String -> String -> Bool
eq = first: second: builtins.compareVersions first second == 0;
## Get the major version from a version string.
##
## @type String -> String
major =
version:
let
parts = builtins.splitVersion version;
in
builtins.elemAt parts 0;
## Get the major version from a version string.
##
## @type String -> String
major =
version:
let
parts = builtins.splitVersion version;
in
builtins.elemAt parts 0;
## Get the minor version from a version string.
##
## @type String -> String
minor =
version:
let
parts = builtins.splitVersion version;
in
builtins.elemAt parts 1;
## Get the minor version from a version string.
##
## @type String -> String
minor =
version:
let
parts = builtins.splitVersion version;
in
builtins.elemAt parts 1;
## Get the patch version from a version string.
##
## @type String -> String
patch =
version:
let
parts = builtins.splitVersion version;
in
builtins.elemAt parts 2;
};
## Get the patch version from a version string.
##
## @type String -> String
patch =
version:
let
parts = builtins.splitVersion version;
in
builtins.elemAt parts 2;
}