chore: initial commit

This commit is contained in:
Jake Hamilton 2024-06-01 04:00:53 -07:00
commit 0409563e32
Signed by: jakehamilton
GPG key ID: 9762169A1B35EA68
28 changed files with 3270 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License Copyright (c) 2024 Aux Contributors
Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

29
README.md Normal file
View file

@ -0,0 +1,29 @@
# Aux Labs
Welcome to Aux Labs! Complimentary beakers and companion
cubes are available in the gift shop.
The Aux Laboratory is a place for experimentation. Here you will find novel
solutions for problems in the Aux world. These experiments are not intended
to be used by anyone yet due to their highly unstable nature. However, we
have decided to publish them here together so that members of the community
may collaborate.
> **Note**: This repository is a part of Aux's early ad-hoc structure. In the
> future we will be moving to a standardized Aux Enhancement Proposal (AEP)
> format.
## Experiment Phases
| Phase | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Idea | An idea exists to solve a problem we currently have with Aux. Send a pull request to this repository creating a new directory for your experiment. The directory should contain a `README.md` file explaining the purpose of the experiment. |
| Iteration | Work on the experiment is done to solve for unknowns and come up with a good solution for the original problem. It may also be helpful to collaborate with others for feedback. |
| Proposal | The experiment has been satisfactorily completed and is ready to be considered for official adoption by the project. Discussion with the relevant Special Interest Group should take place to handle the transition of the project out of the lab and into its own repository. |
| Adoption | The experiment has been adopted and is now a part of Aux! The experiment should be moved to its own repository and the original experiment directory should be deleted |
## Experiments
| Name | Phase | Description |
| ---------------- | ----- | -------------------------------------------------------- |
| [Aux Lib](./lib) | Idea | A library of common functions used in the Aux ecosystem. |

23
lib/LICENSE Normal file
View file

@ -0,0 +1,23 @@
MIT License
Copyright (c) 2003-2023 Eelco Dolstra and the Nixpkgs/NixOS contributors
Copyright (c) 2024 Aux Contributors
Permission is hereby granted, free
of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

104
lib/README.md Normal file
View file

@ -0,0 +1,104 @@
# Aux Lib
Aux Lib is intended to be a replacement for NixPkg's `lib` with stronger constraints around naming,
organization, and inclusion of functions. In addition to replacing library functions, Aux Lib also
defines a revamped version of the NixOS Module system intended to make it easier, more approachable,
and intuitive.
## Usage
The library can be imported both with and without Nix Flakes. To import the library using Nix Flakes,
add this repository as an input.
```nix
inputs.lib.url = "github:auxolotl/labs?dir=lib";
```
To import the library without using Nix Flakes, you will need to use `fetchTarball` and import the
library entrypoint.
```nix
let
labs = builtins.fetchTarball {
url = "https://github.com/auxolotl/labs/archive/main.tar.gz";
sha256 = "<sha256>";
};
lib = import "${labs}/lib";
in
# ...
```
## Development
To contribute to the project, we accept pull requests to this repository. Please see the following
sections for information on the appropriate practices and required steps for working on Aux Lib.
### Documentation
We want our code to survive in the world, but without proper documentation that won't happen. In
order to not lose knowledge and also make it easier for others to begin participating in the
project we require that every function have an appropriate documentation comment. The format for
these comments is as follows:
```nix
let
## This is a description of what the function does. Any necessary information can be added
## here. After this point comes a gap before a Hindley-Milner style type signature. Note
## that these types are not actually checked, but serve as a helpful addition for the user
## in addition to being provided in generated documentation.
##
## @type Int -> String
func = x: builtins.toString x;
in
# ...
```
### Testing
All functions that are added to the project should include tests. The test suites are located
next to their implementation in files ending in `.test.nix`. These tests should ensure that
the library behaves as expected. The typical structure of these test suites is:
```nix
let
lib = import ./../default.nix;
in
{
"my function" = {
"test 1" = let
expected = 1;
input = {};
actual = lib.myFunction input;
in
actual == expected;
};
}
```
Successful tests will return `true` while failing test will resolve with `false`.
### Formatting
All code in this project must be formatted using the provided formatter in the `flake.nix`
file. You can run this formatter using the command `nix fmt`.
### Adding Functionality
Before adding new features to the library, submit an issue or talk the idea over with one or
more of the project maintainers. We want to make sure that the library does not become bloated
with tools that aren't used. Some features may be better handled in a separate project. If you
do get the go-ahead to begin working on your feature, please place it in the library structure
similarly to how existing features are. For example, things dealing with strings should go in
`src/strings/default.nix`.
Additionally, you should prefer to group things in attribute sets for like-functionality. More
broad categories such as `strings` and `lists` are helpful, but scoped groups for things like
`into`, `from`, and `validate` also make the library more discoverable. Having all of the
different parts of the library mirroring this organizational structure makes building intuition
for working with the library much easier. To know when to group new things, consider the
following:
- Would your function name be multiple words like `fromString`?
- Are there multiple variants of this function?
- Would it be easier to find in a group?
- Would grouping help avoid name collisions or confusion?

1
lib/default.nix Normal file
View file

@ -0,0 +1 @@
import ./src

7
lib/flake.nix Normal file
View file

@ -0,0 +1,7 @@
{
description = "A very basic flake";
outputs = _: {
lib = import ./src;
};
}

141
lib/src/attrs/default.nix Normal file
View file

@ -0,0 +1,141 @@
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 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 (
key: values: let
currentPath = path ++ [key];
isSingleValue = builtins.length values == 1;
isComplete =
predicate currentPath
(builtins.elemAt values 1)
(builtins.elemAt values 0);
in
if isSingleValue || isComplete
then builtins.elemAt values 0
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
key = builtins.head path;
rest = builtins.tail path;
in
if path == []
then target
else if target ? ${key}
then lib.attrs.select rest fallback target.${key}
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 target
else error;
# TODO: Document this.
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;
## Check if a path exists in an attribute set.
##
## @type (List String) -> Attrs -> Bool
has = path: target: let
key = builtins.head path;
rest = builtins.tail path;
in
if path == []
then true
else if target ? ${key}
then lib.attrs.has rest target.${key}
else false;
## 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 keys and values to a list.
##
## @type Any a => (String -> Any -> a) -> Attrs -> List a
mapToList = f: target:
builtins.map (key: f key target.${key}) (builtins.attrNames target);
# TODO: Document this.
mapRecursive = f: target:
lib.attrs.mapRecursiveWhen (lib.fp.const true) f target;
# TODO: Document this.
mapRecursiveWhen = predicate: f: target: let
process = path:
builtins.mapAttrs (
key: value:
if builtins.isAttrs value && predicate value
then process (path ++ [key]) value
else f (path ++ [key]) value
);
in
process [] target;
# TODO: Document this.
filter = predicate: target: let
keys = builtins.attrNames target;
process = key: let
value = target.${key};
in
if predicate key value
then [
{
name = key;
value = value;
}
]
else [];
valid = builtins.concatMap process keys;
in
builtins.listToAttrs valid;
};
}

View file

@ -0,0 +1,17 @@
let
lib = import ./../default.nix;
in {
"select" = {
"selects a nested value" = let
expected = "value";
actual =
lib.attrs.select
["x" "y" "z"]
null
{
x.y.z = expected;
};
in
actual == expected;
};
}

48
lib/src/bools/default.nix Normal file
View file

@ -0,0 +1,48 @@
lib: {
bools = {
into = {
string = value:
if value
then "true"
else "false";
};
## 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;
};
}

76
lib/src/default.nix Normal file
View file

@ -0,0 +1,76 @@
let
files = [
./attrs
./bools
./errors
./fp
./generators
./importers
./lists
./math
./modules
./numbers
./options
./packages
./paths
./points
./strings
./types
./versions
];
libs = builtins.map (f: import f) 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
## 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;
## 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
mergeAttrsRecursiveUntil = predicate: x: y: let
process = path:
builtins.zipAttrsWith (
key: values: let
currentPath = path ++ [key];
isSingleValue = builtins.length values == 1;
isComplete =
predicate currentPath
(builtins.elemAt values 1)
(builtins.elemAt values 0);
in
if isSingleValue || isComplete
then builtins.elemAt values 0
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
mergeAttrsRecursive =
mergeAttrsRecursiveUntil
(path: x: y:
!(builtins.isAttrs x && builtins.isAttrs y));
lib = fix (
self: let
merge = acc: create:
mergeAttrsRecursive acc (create self);
in
builtins.foldl' merge {} libs
);
in
lib

View file

@ -0,0 +1,8 @@
lib: {
errors = {
trace = condition: message:
if condition
then true
else builtins.trace message false;
};
}

46
lib/src/fp/default.nix Normal file
View file

@ -0,0 +1,46 @@
lib: {
fp = {
## 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);
## 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 x) 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;
# TODO: Document this.
args = f:
if f ? __functor
then f.__args__ or lib.fp.args (f.__functor f)
else builtins.functionArgs f;
};
}

View file

@ -0,0 +1,144 @@
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'!"
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.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;
};
}

View file

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

160
lib/src/lists/default.nix Normal file
View file

@ -0,0 +1,160 @@
lib: {
lists = {
from = {
# TODO: Document this.
any = value:
if builtins.isList value
then value
else [value];
};
sort = {
## Perform a natural sort on a list of strings.
##
## @type List -> List
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);
};
# TODO: Document this.
mapWithIndex = f: list:
builtins.genList
(i: f i (builtins.elemAt list i))
(builtins.length list);
# TODO: Document this.
mapWithIndex1 = f: list:
builtins.genList
(i: f (i + 1) (builtins.elemAt list i))
(builtins.length list);
## Compare two lists.
##
## @type (a -> b -> Int) -> 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.assertMsg (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;
## 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 [];
# TODO: Document this.
count = predicate: list:
builtins.foldl' (
total: value:
if predicate value
then total + 1
else total
)
0
list;
# TODO: Document this.
unique = list: let
filter = result: value:
if builtins.elem value result
then result
else result ++ [value];
in
builtins.foldl' filter [] list;
};
}

15
lib/src/math/default.nix Normal file
View file

@ -0,0 +1,15 @@
lib: {
math = {
# TODO: Document this.
min = x: y:
if x < y
then x
else y;
# TODO: Document this.
max = x: y:
if x > y
then x
else y;
};
}

600
lib/src/modules/default.nix Normal file
View file

@ -0,0 +1,600 @@
lib: {
modules = {
from = {
## Create a module from a JSON file.
##
## @type Path -> Module
json = file: {
__file__ = file;
config = lib.importers.json file;
};
## Create a module from a TOML file.
##
## @type Path -> Module
toml = file: {
__file__ = file;
config = lib.importers.toml file;
};
};
apply = {
# TODO: Document this.
properties = definition:
if lib.types.is "merge" definition
then builtins.concatMap lib.modules.apply.properties definition.content
else if lib.types.is "when" definition
then
if !(builtins.isBool definition.condition)
then builtins.throw "lib.modules.when called with a non-boolean condition"
else if definition.condition
then lib.modules.apply.properties definition.content
else []
else [definition];
# TODO: Document this.
overrides = definitions: let
getPriority = definition:
if lib.types.is "override" definition.value
then definition.value.priority
else lib.modules.DEFAULT_PRIORITY;
normalize = definition:
if lib.types.is "override" definition.value
then
definition
// {
value = definition.value.content;
}
else definition;
highestPriority =
builtins.foldl'
(priority: definition: lib.math.min priority (getPriority definition))
9999
definitions;
in {
inherit highestPriority;
values =
builtins.concatMap
(
definition:
if getPriority definition == highestPriority
then [(normalize definition)]
else []
)
definitions;
};
# TODO: Document this.
order = definitions: let
normalize = definition:
if lib.types.is "order" definition
then
definition
// {
value = definition.value.content;
priority = definition.value.priority;
}
else definition;
normalized = builtins.map normalize definitions;
compare = a: b: (a.priority or lib.modules.DEFAULT_PRIORITY) < (b.priority or lib.modules.DEFAULT_PRIORITY);
in
builtins.sort compare normalized;
# TODO: Document this.
fixup = location: option:
if option.type.getSubModules or null == null
then
option
// {
type = option.type or lib.types.unspecified;
}
else
option
// {
type = option.type.withSubModules option.options;
options = [];
};
# TODO: Document this.
invert = config:
if lib.types.is "merge" config
then builtins.concatMap lib.modules.apply.invert config.content
else if lib.types.is "when" config
then
builtins.map
(builtins.mapAttrs (key: value: lib.modules.when config.condition value))
(lib.modules.apply.invert config.content)
else if lib.types.is "override" config
then
builtins.map
(builtins.mapAttrs (key: value: lib.modules.override config.priority value))
(lib.modules.apply.invert config.content)
else [config];
};
validate = {
# TODO: Document this.
keys = module: let
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
in
invalid == {};
};
# TODO: Document this.
VALID_KEYS = [
"__file__"
"__key__"
"includes"
"excludes"
"options"
"config"
"freeform"
"meta"
];
# TODO: Document this.
normalize = file: key: module: let
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
in
if lib.modules.validate.keys module
then {
__file__ = builtins.toString module.__file__ or file;
__key__ = builtins.toString module.__key__ or key;
includes = module.includes or [];
excludes = module.excludes or [];
options = module.options or {};
config = let
base = module.config or {};
withMeta = config:
if module ? meta
then lib.modules.merge [config {meta = module.meta;}]
else config;
withFreeform = config:
if module ? freeform
then lib.modules.merge [config {__module__.freeform = module.freeform;}]
else config;
in
withFreeform (withMeta base);
}
else builtins.throw "Module `${key}` has unsupported attribute(s): ${invalidKeys}";
# TODO: Document this.
resolve = key: module: args: let
dynamicArgs =
builtins.mapAttrs
(
name: value:
builtins.addErrorContext
"while evaluating the module argument `${name}` in `${key}`"
(args.${name} or args.config.__module__.args.dynamic.${name})
)
(lib.fp.args module);
in
if builtins.isFunction module
then module (args // dynamicArgs)
else module;
# TODO: Document this.
DEFAULT_PRIORITY = 100;
## Allow for sorting the values provided to a module by priority. The
## most important value will be used.
##
## @type Int -> a -> Priority a
order = priority: value: {
__type__ = "order";
inherit priority value;
};
orders = {
# TODO: Document this.
before = lib.modules.order 500;
# TODO: Document this.
default = lib.modules.order 1000;
# TODO: Document this.
after = lib.modules.order 1500;
};
## Extract a list of files from a list of modules.
##
## @type List Module -> List (String | Path)
getFiles = builtins.map (module: module.__file__);
# TODO: Document this.
when = condition: content: {
__type__ = "when";
inherit condition content;
};
# TODO: Document this.
merge = content: {
__type__ = "merge";
inherit content;
};
# TODO: Document this.
override = priority: content: {
__type__ = "override";
inherit priority content;
};
overrides = {
# TODO: Document this.
option = lib.modules.override 1500;
# TODO: Document this.
default = lib.modules.override 1000;
# TODO: Document this.
force = lib.modules.override 50;
# TODO: Document this.
vm = lib.modules.override 10;
};
# TODO: Document this.
combine = prefix: modules: let
getConfig = module:
builtins.map
(config: {
__file__ = module.__file__;
inherit config;
})
(lib.modules.apply.invert module.config);
configs =
builtins.concatMap
getConfig
modules;
process = prefix: options: configs: let
# TODO: Document this.
byName = attr: f: modules:
builtins.zipAttrsWith
(lib.fp.const builtins.concatLists)
(builtins.map (
module: let
subtree = module.${attr};
in
if builtins.isAttrs subtree
then builtins.mapAttrs (key: f module) subtree
else builtins.throw "Value for `${builtins.concatStringsSep "." prefix} is of type `${builtins.typeOf subtree}` but an attribute set was expected."
)
modules);
declarationsByName =
byName
"options"
(module: option: [
{
__file__ = module.__file__;
options = option;
}
])
options;
definitionsByName =
byName
"config"
(
module: value:
builtins.map
(config: {
__file__ = module.__file__;
inherit config;
})
(lib.modules.apply.invert value)
)
configs;
definitionsByName' =
byName
"config"
(module: value: [
{
__file__ = module.__file__;
inherit value;
}
])
configs;
getOptionFromDeclaration = declaration:
if lib.types.is "option" declaration.options
then declaration
else
declaration
// {
options = lib.options.create {
type = lib.types.submodule [{options = declaration.options;}];
};
};
resultsByName =
builtins.mapAttrs
(
name: declarations: let
location = prefix ++ [name];
definitions = definitionsByName.${name} or [];
definitions' = definitionsByName'.${name} or [];
optionDeclarations =
builtins.filter
(declaration: lib.types.is "option" declaration.options)
declarations;
in
if builtins.length optionDeclarations == builtins.length declarations
then let
option =
lib.modules.apply.fixup
location
(lib.options.merge.declarations location declarations);
in {
matched = lib.options.run location option definitions';
unmatched = [];
}
else if optionDeclarations != []
then
if builtins.all (declaration: declaration.options.type.name == "Submodule") optionDeclarations
then let
option =
lib.modules.apply.fixup location
(lib.options.merge.declarations location (builtins.map getOptionFromDeclaration declarations));
in {
matched = lib.options.run location option definitions';
unmatched = [];
}
else builtins.throw "The option `${lib.options.getIdentifier location}` in module `${(builtins.head optionDeclarations).__file__}` does not support nested options."
else process location declarations definitions
)
declarationsByName;
matched = builtins.mapAttrs (key: value: value.matched) resultsByName;
unmatched =
builtins.mapAttrs (key: value: value.unmatched) resultsByName
// builtins.removeAttrs definitionsByName' (builtins.attrNames matched);
in {
inherit matched;
unmatched =
if configs == []
then []
else
builtins.concatLists (
lib.attrs.mapToList
(
name: definitions:
builtins.map (definition:
definition
// {
prefix = [name] ++ (definition.prefix or []);
})
definitions
)
unmatched
);
};
in
process prefix modules configs;
# TODO: Document this.
run = settings @ {
modules ? [],
args ? {},
prefix ? [],
}: let
type = lib.types.submodules.of {
inherit modules args;
};
extend = extensions @ {
modules ? [],
args ? {},
prefix ? [],
}:
lib.modules.run {
modules = settings.modules ++ extensions.modules;
args = (settings.args or {}) // extensions.args;
prefix = extensions.prefix or settings.prefix or [];
};
# TODO: Document this.
collect = let
load = args: file: key: module: let
moduleFromValue = lib.modules.normalize file key (lib.modules.resolve key module args);
moduleFromPath =
lib.modules.normalize
(builtins.toString module)
(builtins.toString module)
(lib.modules.resolve (builtins.toString module) (import module) args);
in
if builtins.isAttrs module || builtins.isFunction module
then moduleFromValue
else if builtins.isString module || builtins.isPath module
then moduleFromPath
else builtins.throw "The provided module must be either an attribute set, function, or path but got ${builtins.typeOf module}";
normalize = parentFile: parentKey: modules: args: let
normalized =
lib.lists.mapWithIndex1
(
i: value: let
module = load args parentFile "${parentKey}:unknown-${builtins.toString i}" value;
tree = normalize module.__file__ module.__key__ module.includes args;
in {
inherit module;
key = module.__key__;
modules = tree.modules;
excludes = module.excludes ++ tree.excludes;
}
)
modules;
in {
modules = normalized;
excludes = builtins.concatLists (builtins.catAttrs "excludes" normalized);
};
withExclusions = path: {
modules,
excludes,
}: let
getKey = module:
if builtins.isString module && (builtins.substring 0 1 module != "/")
then (builtins.toString path) + "/" + module
else builtins.toString module;
excludedKeys = builtins.map getKey excludes;
removeExcludes =
builtins.filter
(value: !(builtins.elem value.key excludedKeys));
in
builtins.map
(value: value.module)
(builtins.genericClosure {
startSet = removeExcludes modules;
operator = value: removeExcludes value.modules;
});
process = path: modules: args:
withExclusions path (normalize "<unknown>" "" modules args);
in
process;
internal = {
__file__ = "virtual:aux/internal";
__key__ = "virtual:aux/internal";
options = {
__module__ = {
args = {
static = lib.options.create {
type = lib.types.attrs.lazy lib.types.raw;
writable = false;
internal = false;
description = "Static arguments provided to lib.modules.run which cannot be changed.";
};
dynamic = lib.options.create {
type = lib.types.attrs.lazy lib.types.raw;
${
if prefix == []
then null
else "internal"
} =
true;
visible = false;
description = "Additional arguments pased to each module.";
};
};
check = lib.options.create {
type = lib.types.bool;
default.value = true;
internal = true;
description = "Whether to perform checks on option definitions.";
};
freeform = lib.options.create {
type = lib.types.nullish lib.types.option;
default.value = null;
internal = true;
description = "If set, all options that don't have a declared type will be merged using this type.";
};
};
};
config = {
__module__ = {
args = {
static = args;
dynamic = {
meta = {
inherit extend type;
};
};
};
};
};
};
merged = let
collected =
collect
(args.path or "")
(modules ++ [internal])
({inherit lib options config;} // args);
in
lib.modules.combine prefix (lib.lists.reverse collected);
options = merged.matched;
config = let
declared =
lib.attrs.mapRecursiveWhen
(value: !(lib.types.is "option" value))
(key: value: value.value)
options;
freeform = let
definitions =
builtins.map
(definition: {
__file__ = definition.__file__;
value = lib.attrs.set definition.prefix definition.value;
})
merged.unmatched;
in
if definitions == []
then {}
else declared.__module__.freeform.merge prefix definitions;
in
if declared.__module__.freeform == null
then declared
else
lib.attrs.mergeRecursive
freeform
declared;
checked =
if config.__module__.check && config.__module__.freeform == null && merged.unmatched != []
then let
first = builtins.head merged.unmatched;
identifier = lib.options.getIdentifier (prefix ++ first.prefix);
definitions =
builtins.addErrorContext "while evaluating the error message for definitions of non-existent option `${identifier}`"
(
builtins.addErrorContext "while evaluating a definition from `${first.__file__}`"
(lib.options.getDefinitions [first])
);
message = "The option `${identifier}` does not exist. Definitions:${definitions}";
in
if builtins.attrNames options == ["__module__"]
then
if lib.options.getIdentifier prefix == ""
then
builtins.throw ''
${message}
You are trying to declare options in `config` rather than `options`.
''
else
builtins.throw ''
${message}
There are no options defined in `${lib.options.getIdentifier prefix}`.
Are you sure you declared your options correctly?
''
else builtins.throw message
else null;
withCheck = builtins.seq checked;
in {
inherit type extend;
options = withCheck options;
config = withCheck (builtins.removeAttrs config ["__module__"]);
__module__ = withCheck config.__module__;
};
};
}

View file

@ -0,0 +1,28 @@
let
lib = import ./../default.nix;
in {