Import all nix files in a directory tree.
Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/
dendrix.oeiuwq.com/Dendritic.html
dendritic
inputs
1let
2 perform =
3 {
4 lib ? null,
5 pipef ? null,
6 initf ? null,
7 filterf,
8 mapf,
9 paths,
10 ...
11 }:
12 path:
13 let
14 result =
15 if pipef == null then
16 { imports = [ module ]; }
17 else if lib == null then
18 throw "You need to call withLib before trying to read the tree."
19 else
20 pipef (leafs lib path);
21
22 # module exists so we delay access to lib til we are part of the module system.
23 module =
24 { lib, ... }:
25 {
26 imports = leafs lib path;
27 };
28
29 leafs =
30 lib:
31 let
32 treeFiles = t: (t.withLib lib).files;
33
34 listFilesRecursive =
35 x:
36 if isImportTree x then
37 treeFiles x
38 else if hasOutPath x then
39 listFilesRecursive x.outPath
40 else if isDirectory x then
41 lib.filesystem.listFilesRecursive x
42 else
43 [ x ];
44
45 nixFilter = andNot (lib.hasInfix "/_") (lib.hasSuffix ".nix");
46
47 initialFilter = if initf != null then initf else nixFilter;
48
49 pathFilter = compose (and filterf initialFilter) toString;
50
51 otherFilter = and filterf (if initf != null then initf else (_: true));
52
53 filter = x: if isPathLike x then pathFilter x else otherFilter x;
54
55 isFileRelative =
56 root:
57 { file, rel }:
58 if file != null && lib.hasPrefix root file then
59 {
60 file = null;
61 rel = lib.removePrefix root file;
62 }
63 else
64 { inherit file rel; };
65 getFileRelative = { file, rel }: if rel == null then file else rel;
66
67 makeRelative =
68 roots:
69 lib.pipe roots [
70 (lib.lists.flatten)
71 (builtins.filter isDirectory)
72 (builtins.map builtins.toString)
73 (builtins.map isFileRelative)
74 (fx: fx ++ [ getFileRelative ])
75 (
76 fx: file:
77 lib.pipe {
78 file = builtins.toString file;
79 rel = null;
80 } fx
81 )
82 ];
83
84 rootRelative =
85 roots:
86 let
87 mkRel = makeRelative roots;
88 in
89 x: if isPathLike x then mkRel x else x;
90 in
91 root:
92 lib.pipe
93 [ paths root ]
94 [
95 (lib.lists.flatten)
96 (map listFilesRecursive)
97 (lib.lists.flatten)
98 (builtins.filter (
99 compose filter (rootRelative [
100 paths
101 root
102 ])
103 ))
104 (map mapf)
105 ];
106
107 in
108 result;
109
110 compose =
111 g: f: x:
112 g (f x);
113
114 # Applies the second filter first, to allow partial application when building the configuration.
115 and =
116 g: f: x:
117 f x && g x;
118
119 andNot = g: and (x: !(g x));
120
121 matchesRegex = re: p: builtins.match re p != null;
122
123 mapAttr =
124 attrs: k: f:
125 attrs // { ${k} = f attrs.${k}; };
126
127 isDirectory = and (x: builtins.readFileType x == "directory") isPathLike;
128
129 isPathLike = x: builtins.isPath x || builtins.isString x || hasOutPath x;
130
131 hasOutPath = and (x: x ? outPath) builtins.isAttrs;
132
133 isImportTree = and (x: x ? __config.__functor) builtins.isAttrs;
134
135 inModuleEval = and (x: x ? options) builtins.isAttrs;
136
137 functor = self: arg: perform self.__config (if inModuleEval arg then [ ] else arg);
138
139 callable =
140 let
141 initial = {
142 # Accumulated configuration
143 api = { };
144 mapf = (i: i);
145 filterf = _: true;
146 paths = [ ];
147
148 # config is our state (initial at first). this functor allows it
149 # to work as if it was a function, taking an update function
150 # that will return a new state. for example:
151 # in mergeAttrs: `config (c: c // x)` will merge x into new config.
152 __functor =
153 config: update:
154 let
155 # updated is another config
156 updated = update config;
157
158 # current is the result of this functor.
159 # it is not a config, but an import-tree object containing a __config.
160 current = config update;
161 boundAPI = builtins.mapAttrs (_: g: g current) updated.api;
162
163 # these two helpers are used to **append** aggregated configs.
164 accAttr = attrName: acc: config (c: mapAttr (update c) attrName acc);
165 mergeAttrs = attrs: config (c: (update c) // attrs);
166 in
167 boundAPI
168 // {
169 __config = updated;
170 __functor = functor; # user-facing callable
171
172 # Configuration updates (accumulating)
173 filter = filterf: accAttr "filterf" (and filterf);
174 filterNot = filterf: accAttr "filterf" (andNot filterf);
175 match = regex: accAttr "filterf" (and (matchesRegex regex));
176 matchNot = regex: accAttr "filterf" (andNot (matchesRegex regex));
177 map = mapf: accAttr "mapf" (compose mapf);
178 addPath = path: accAttr "paths" (p: p ++ [ path ]);
179 addAPI = api: accAttr "api" (a: a // api);
180
181 # Configuration updates (non-accumulating)
182 withLib = lib: mergeAttrs { inherit lib; };
183 initFilter = initf: mergeAttrs { inherit initf; };
184 pipeTo = pipef: mergeAttrs { inherit pipef; };
185 leafs = mergeAttrs { pipef = (i: i); };
186
187 # Applies empty (for already path-configured trees)
188 result = current [ ];
189
190 # Return a list of all filtered files.
191 files = current.leafs.result;
192
193 # returns the original empty state
194 new = callable;
195 };
196 };
197 in
198 initial (config: config);
199
200in
201callable