feat: dag, internal inputs solution, license update

This commit is contained in:
Jake Hamilton 2024-06-12 02:04:53 -07:00
parent f24f0876a9
commit 04bc516868
Signed by: jakehamilton
GPG key ID: 9762169A1B35EA68
14 changed files with 511 additions and 35 deletions

View file

@ -27,4 +27,4 @@ may collaborate.
| Name | Phase | Description |
| ------------------------------ | --------- | -------------------------------------------------------------------------- |
| [Aux Lib](./lib) | Iteration | A library of common functions used in the Aux ecosystem. |
| [Aux Foundation](./foundation) | Idea | Foundational packages which allow for bootstrapping a greater package set. |
| [Aux Foundation](./foundation) | Iteration | Foundational packages which allow for bootstrapping a greater package set. |

View file

@ -1,6 +1,26 @@
{
"nodes": {
"root": {}
"lib": {
"locked": {
"dir": "lib",
"dirtyRev": "f24f0876a9103c7adb8120ce9709fb90c73f2a7c-dirty",
"dirtyShortRev": "f24f087-dirty",
"lastModified": 1718105966,
"narHash": "sha256-L68G29+bPmwZSERg3VYXdfont/w+mssmWnrs6tyBijk=",
"type": "git",
"url": "file:../?dir=lib"
},
"original": {
"dir": "lib",
"type": "git",
"url": "file:../?dir=lib"
}
},
"root": {
"inputs": {
"lib": "lib"
}
}
},
"root": "root",
"version": 7

View file

@ -2,16 +2,13 @@
description = "A set of foundational packages required for bootstrapping a larger package set.";
inputs = {
# TODO: When this project is moved to its own repository we will want to add
# inputs for the relevant dependencies.
# lib = {
# url = "path:../lib";
# };
lib = {
url = "git+file:../?dir=lib";
};
};
outputs = inputs: let
# inherit (inputs.lib) lib;
lib = import ./../lib;
inherit (inputs.lib) lib;
modules = import ./src;

View file

@ -1,5 +1,6 @@
MIT License
Copyright (c) 2003-2023 Eelco Dolstra and the Nixpkgs/NixOS contributors
Copyright (c) 2017-2023 Home Manager contributors
Copyright (c) 2024 Aux Contributors
Permission is hereby granted, free

View file

@ -7,6 +7,7 @@ let
./src/default.test.nix
./src/attrs/default.test.nix
./src/bools/default.test.nix
./src/dag/default.test.nix
./src/errors/default.test.nix
./src/fp/default.test.nix
./src/generators/default.test.nix

138
lib/src/dag/default.nix Normal file
View file

@ -0,0 +1,138 @@
lib: {
dag = {
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;
};
sort = {
## Apply a topological sort to a DAG.
##
## @type Dag a -> { result :: List a } | { cycle :: List a, loops :: List a }
topographic = 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;
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.topographic isBefore entries;
in
if sorted ? result
then {
result =
builtins.map (value: {
name = value.name;
value = value.value;
})
sorted.result;
}
else sorted;
};
## 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 List String -> List String -> a -> { before :: List String, after :: List String, value :: a }
between = before: after: 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 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 = lib.dag.entry.between [];
};
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: before: after: 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 before after 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 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: lib.dag.entries.between tag [];
};
};
}

View file

@ -0,0 +1,135 @@
let
lib = import ./../default.nix;
in {
"validate" = {
"entry" = {
"invalid value" = let
expected = false;
actual = lib.dag.validate.entry {};
in
actual == expected;
"a manually created value" = let
expected = true;
actual = lib.dag.validate.entry {
value = null;
before = [];
after = [];
};
in
actual == expected;
"entry.between" = let
expected = true;
actual = lib.dag.validate.entry (lib.dag.entry.between [] [] null);
in
actual == expected;
"entry.anywhere" = let
expected = true;
actual = lib.dag.validate.entry (lib.dag.entry.anywhere null);
in
actual == expected;
"entry.before" = let
expected = true;
actual = lib.dag.validate.entry (lib.dag.entry.before [] null);
in
actual == expected;
"entry.after" = let
expected = true;
actual = lib.dag.validate.entry (lib.dag.entry.after [] null);
in
actual == expected;
};
"graph" = {
"invalid value" = let
expected = false;
actual = lib.dag.validate.graph {
x = {};
};
in
actual == expected;
"a manually created value" = let
expected = true;
actual = lib.dag.validate.graph {
x = {
value = null;
before = [];
after = [];
};
};
in
actual == expected;
"entries.between" = let
expected = true;
graph = lib.dag.entries.between "example" [] [] [null null];
actual = lib.dag.validate.graph graph;
in
actual == expected;
"entries.anywhere" = let
expected = true;
graph = lib.dag.entries.anywhere "example" [null null];
actual = lib.dag.validate.graph graph;
in
actual == expected;
"entries.before" = let
expected = true;
graph = lib.dag.entries.before "example" [] [null null];
actual = lib.dag.validate.graph graph;
in
actual == expected;
"entries.after" = let
expected = true;
graph = lib.dag.entries.after "example" [] [null null];
actual = lib.dag.validate.graph graph;
in
actual == expected;
};
};
"sort" = {
"topographic" = {
"handles an empty graph" = let
expected = [];
actual = lib.dag.sort.topographic {};
in
actual.result == expected;
"sorts a graph" = let
expected = [
{
name = "a";
value = "a";
}
{
name = "b";
value = "b";
}
{
name = "c";
value = "c";
}
{
name = "d";
value = "d";
}
];
actual = lib.dag.sort.topographic {
a = lib.dag.entry.anywhere "a";
b = lib.dag.entry.between ["c"] ["a"] "b";
c = lib.dag.entry.before ["c"] "c";
d = lib.dag.entry.after ["c"] "d";
};
in
actual.result == expected;
};
};
}

View file

@ -2,6 +2,7 @@ let
files = [
./attrs
./bools
./dag
./errors
./fp
./generators

View file

@ -29,6 +29,56 @@ lib: {
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 topographic 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
topographic = predicate: list: let
searched = lib.lists.search.depthFirst true predicate list;
results = lib.lists.sort.topographic 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);
};
## Map a list using both the index and value of each item. The

View file

@ -130,6 +130,54 @@ lib: {
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.
##

View file

@ -1036,4 +1036,71 @@ lib: {
};
};
};
dag = {
## Create a type that allows a DAG (Directed Acyclic Graph) of a given type.
##
## @type Attrs -> Attrs
of = type: let
resolved = lib.types.attrs.of (lib.types.dag.entry type);
in
lib.types.create {
name = "Dag";
description = "Dag of ${type.description}";
check = resolved.check;
merge = resolved.merge;
fallback = resolved.fallback;
getSubOptions = prefix: type.getSubOptions (prefix ++ ["<name>"]);
getSubModules = type.getSubModules;
withSubModules = modules: lib.types.dag.of (type.withSubModules modules);
functor = lib.types.functor "dag.of" // {wrapped = type;};
children = {
element = type;
resolved = resolved;
};
};
## Create a type that allows a DAG entry of a given type.
##
## @type Attrs -> Attrs
entry = type: let
submodule = lib.types.submodule ({name}: {
options = {
value = lib.options.create {
type = type;
};
before = lib.options.create {
type = lib.types.list.of lib.types.string;
};
after = lib.options.create {
type = lib.types.list.of lib.types.string;
};
};
});
normalize = definition: let
value =
if definition ? priority
then lib.modules.order definition.priority definition.value
else definition.value;
in
if lib.dag.validate.entry definition.value
then definition.value
else lib.dag.entry.anywhere value;
in
lib.types.create {
name = "DagEntry";
description = "DagEntry (${type.description})";
merge = location: definitions:
submodule.merge
location
(
builtins.map
(definition: {
__file__ = definition.__file__;
value = normalize definition;
})
definitions
);
};
};
}

View file

@ -7,26 +7,34 @@
]
},
"locked": {
"lastModified": 1,
"narHash": "sha256-CDfGWoJg+7i9z6quwh/WXuTkOhqKme73dUTvWCvJ8EA=",
"path": "../foundation",
"type": "path"
"dir": "foundation",
"dirtyRev": "f24f0876a9103c7adb8120ce9709fb90c73f2a7c-dirty",
"dirtyShortRev": "f24f087-dirty",
"lastModified": 1718105966,
"narHash": "sha256-cvGKyJzoPAXjuM+YDpQM30qwshgoYZmAYtJywFPyfGI=",
"type": "git",
"url": "file:../?dir=foundation"
},
"original": {
"path": "../foundation",
"type": "path"
"dir": "foundation",
"type": "git",
"url": "file:../?dir=foundation"
}
},
"lib": {
"locked": {
"lastModified": 1,
"narHash": "sha256-TtgysWL53BK3f3JrPFunjzJaXWaDG2RbuPMglCwATOY=",
"path": "../lib",
"type": "path"
"dir": "lib",
"dirtyRev": "f24f0876a9103c7adb8120ce9709fb90c73f2a7c-dirty",
"dirtyShortRev": "f24f087-dirty",
"lastModified": 1718105966,
"narHash": "sha256-cvGKyJzoPAXjuM+YDpQM30qwshgoYZmAYtJywFPyfGI=",
"type": "git",
"url": "file:../?dir=lib"
},
"original": {
"path": "../lib",
"type": "path"
"dir": "lib",
"type": "git",
"url": "file:../?dir=lib"
}
},
"root": {

View file

@ -1,20 +1,18 @@
{
inputs = {
# TODO: When this project is moved to its own repository we will want to add
# inputs for the relevant dependencies.
# lib = {
# url = "path:../lib";
# };
# foundation = {
# url = "path:../foundation";
# inputs.lib.follows = "lib";
# };
lib = {
url = "git+file:../?dir=lib";
};
foundation = {
url = "git+file:../?dir=foundation";
inputs.lib.follows = "lib";
};
};
outputs = inputs: let
exports = import ./default.nix {
# lib = inputs.lib.lib;
# foundation = inputs.foundation.packages.i686-linux;
lib = inputs.lib.lib;
foundation = inputs.foundation.packages.i686-linux;
};
in
exports;

View file

@ -58,7 +58,8 @@ in {
};
description = lib.options.create {
type = lib.types.string;
type = lib.types.nullish lib.types.string;
default.value = null;
description = "The description for the package.";
};
@ -112,7 +113,7 @@ in {
name = lib.options.create {
type = lib.types.string;
default = {
value = "${config.pname}-${config.version}";
value = "${config.pname}-${config.version or "unknown"}";
text = "\${config.pname}-\${config.version}";
};
description = "The name of the package.";
@ -132,6 +133,13 @@ in {
meta = lib.options.create {
type = lib'.types.meta;
default = {
text = "{ name = <package>.pname; }";
value = {
name = config.pname;
};
};
description = "The metadata for the package.";
};
env = lib.options.create {
@ -141,7 +149,11 @@ in {
};
phases = lib.options.create {
type = lib.types.attrs.of lib.types.string;
type = lib.types.dag.of (
lib.types.either
lib.types.string
(lib.types.function lib.types.string)
);
default.value = {};
description = "Phases for the package's builder to use.";
};