From 04bc516868c70a832f52d886af961049ae6a7b78 Mon Sep 17 00:00:00 2001 From: Jake Hamilton Date: Wed, 12 Jun 2024 02:04:53 -0700 Subject: [PATCH] feat: dag, internal inputs solution, license update --- README.md | 2 +- foundation/flake.lock | 22 +++++- foundation/flake.nix | 11 +-- lib/LICENSE | 1 + lib/default.test.nix | 1 + lib/src/dag/default.nix | 138 +++++++++++++++++++++++++++++++++++ lib/src/dag/default.test.nix | 135 ++++++++++++++++++++++++++++++++++ lib/src/default.nix | 1 + lib/src/lists/default.nix | 50 +++++++++++++ lib/src/strings/default.nix | 48 ++++++++++++ lib/src/types/default.nix | 67 +++++++++++++++++ potluck/flake.lock | 32 +++++--- potluck/flake.nix | 20 +++-- potluck/src/lib/types.nix | 18 ++++- 14 files changed, 511 insertions(+), 35 deletions(-) create mode 100644 lib/src/dag/default.nix create mode 100644 lib/src/dag/default.test.nix diff --git a/README.md b/README.md index f75f1f7..b0d54a6 100644 --- a/README.md +++ b/README.md @@ -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. | diff --git a/foundation/flake.lock b/foundation/flake.lock index 5999137..6592f75 100644 --- a/foundation/flake.lock +++ b/foundation/flake.lock @@ -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 diff --git a/foundation/flake.nix b/foundation/flake.nix index d6e3e0c..d2bcde4 100644 --- a/foundation/flake.nix +++ b/foundation/flake.nix @@ -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; diff --git a/lib/LICENSE b/lib/LICENSE index 22c5fd5..46376b6 100644 --- a/lib/LICENSE +++ b/lib/LICENSE @@ -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 diff --git a/lib/default.test.nix b/lib/default.test.nix index a715d06..8f243b3 100644 --- a/lib/default.test.nix +++ b/lib/default.test.nix @@ -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 diff --git a/lib/src/dag/default.nix b/lib/src/dag/default.nix new file mode 100644 index 0000000..432ded3 --- /dev/null +++ b/lib/src/dag/default.nix @@ -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 []; + }; + }; +} diff --git a/lib/src/dag/default.test.nix b/lib/src/dag/default.test.nix new file mode 100644 index 0000000..f6f4076 --- /dev/null +++ b/lib/src/dag/default.test.nix @@ -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; + }; + }; +} diff --git a/lib/src/default.nix b/lib/src/default.nix index a92b2d9..13ba6d0 100644 --- a/lib/src/default.nix +++ b/lib/src/default.nix @@ -2,6 +2,7 @@ let files = [ ./attrs ./bools + ./dag ./errors ./fp ./generators diff --git a/lib/src/lists/default.nix b/lib/src/lists/default.nix index 1d86b15..ec5bd4e 100644 --- a/lib/src/lists/default.nix +++ b/lib/src/lists/default.nix @@ -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 diff --git a/lib/src/strings/default.nix b/lib/src/strings/default.nix index 572c2b9..b427139 100644 --- a/lib/src/strings/default.nix +++ b/lib/src/strings/default.nix @@ -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. ## diff --git a/lib/src/types/default.nix b/lib/src/types/default.nix index 191caf9..050d0b1 100644 --- a/lib/src/types/default.nix +++ b/lib/src/types/default.nix @@ -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 ++ [""]); + 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 + ); + }; + }; } diff --git a/potluck/flake.lock b/potluck/flake.lock index eb4db76..3f324f2 100644 --- a/potluck/flake.lock +++ b/potluck/flake.lock @@ -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": { diff --git a/potluck/flake.nix b/potluck/flake.nix index f797c5f..e98c783 100644 --- a/potluck/flake.nix +++ b/potluck/flake.nix @@ -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; diff --git a/potluck/src/lib/types.nix b/potluck/src/lib/types.nix index 0ca17c2..00f662c 100644 --- a/potluck/src/lib/types.nix +++ b/potluck/src/lib/types.nix @@ -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 = .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."; };