Skip to content

Commit 5bb8e89

Browse files
committed
lib/lazyloader: [BROKEN] Introduce lazyloader
Implement functionality that lazily loads modules (by ab)using the `freeformType` option"
1 parent baf1e9a commit 5bb8e89

File tree

4 files changed

+329
-2
lines changed

4 files changed

+329
-2
lines changed

modules/deprecated.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

modules/hjem.nix

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,49 @@
33
rumLib,
44
inputs,
55
}: {
6+
config,
7+
options,
8+
...
9+
}: let
10+
resolvedConfig =
11+
rumLib.modules.resolveModulesFromLazyModule {
12+
modulesDir = ./collection;
13+
deferredModule = config.rum;
14+
extraModules = [
15+
inputs.hjem.nixosModules.hjem-lib
16+
{_module.args.rumLib = rumLib;}
17+
(
18+
{
19+
config,
20+
options,
21+
...
22+
}: {
23+
options = options.rum;
24+
config.rum = builtins.removeAttrs config ["rum"];
25+
}
26+
)
27+
./deprecated.nix
28+
];
29+
inherit rumLib options;
30+
}
31+
// {
32+
_module.args.rumLib = rumLib;
33+
};
34+
35+
usedConfig = builtins.removeAttrs resolvedConfig ["rum"];
36+
in {
637
# Import the Hjem Rum module collection as an extraModule available under `hjem.users.<username>`
738
# This allows the definition of rum modules under `hjem.users.<username>.rum`
839

940
# Import the collection modules recursively so that all files
1041
# are imported. This then gets imported into the user's
1142
# 'hjem.extraModules' to make them available under 'hjem.users.<username>'
12-
imports = [inputs.hjem.nixosModules.hjem-lib] ++ lib.filesystem.listFilesRecursive ./collection;
43+
options.rum = lib.mkOption {
44+
type = lib.types.deferredModule;
45+
default = {};
46+
};
1347

14-
_module.args.rumLib = rumLib;
48+
config = {
49+
files = usedConfig;
50+
};
1551
}

modules/lib/black_magic.nix

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
{lib}: let
2+
inherit
3+
(builtins)
4+
filter
5+
isAttrs
6+
isNull
7+
isString
8+
concatLists
9+
head
10+
readDir
11+
mapAttrs
12+
length
13+
hasAttr
14+
getAttr
15+
concatStringsSep
16+
attrNames
17+
attrValues
18+
;
19+
inherit
20+
(lib.attrsets)
21+
mapAttrsToList
22+
showAttrPath
23+
attrByPath
24+
mergeAttrsList
25+
filterAttrs
26+
recursiveUpdate
27+
;
28+
inherit
29+
(lib.lists)
30+
last
31+
drop
32+
dropEnd
33+
fold
34+
;
35+
inherit (lib.modules) evalModules;
36+
/**
37+
This is effectively (builtins.tail list), however even the docs themselves state
38+
to avoid that function due to an operation cost of O(n) instead of O(1) per call.
39+
40+
tail :: [ T ] -> [ T ]
41+
*/
42+
tail = list: (drop 1 list);
43+
/**
44+
A bit of terminology to prevent possible confusion of myself and others in the future:
45+
path: An attribute path,
46+
e.g.
47+
{ a = { b = "c"; }; } => [ "a" "b" ]
48+
or
49+
{ a = { b = mkOption {...}; }; } => [ "a" "b" ]
50+
51+
filetree:
52+
Just your regular directory with possibly nested directories.
53+
filetree != path (in this context)
54+
55+
Some custom types used throughout the annotations:
56+
57+
filetree :: { ${pathComponent} :: (filetree | ${file}}) }
58+
attrPath :: [ string ]
59+
moduleResolutionResult :: { resolved :: [ ${filepath} ]; unresolved :: [ ${attrPath} ] }
60+
*/
61+
62+
resolveFileTreeRecursive = path: let
63+
dirItems = readDir path;
64+
in
65+
mapAttrs (
66+
name: value: let
67+
ItemPath = path + ("/" + name);
68+
in
69+
if value == "directory"
70+
then resolveFileTreeRecursive ItemPath
71+
else ItemPath
72+
)
73+
dirItems;
74+
75+
/**
76+
pop the last element from the list
77+
78+
pop :: [ T ] -> [ T ]
79+
*/
80+
81+
/**
82+
If the current attrset is a final value, return an empty path as there are no child paths.
83+
Else, recurse into each child value, get their paths, add the child name to the path,
84+
and combine all child paths into one list
85+
86+
getAttrPaths' :: { ... :: ?; _type ? :: string } -> [ attrPath | [ string | 1 ] ]
87+
*/
88+
getAttrPaths' = attrset:
89+
if !(isAttrs attrset) || (attrset ? _type && isString attrset._type)
90+
then
91+
if isAttrs attrset && attrset._type == "if"
92+
then [[1]] # No fucking idea what do do here instead of throwing, I dont think this can happen though.
93+
else [[]]
94+
else
95+
concatLists (
96+
mapAttrsToList (name: value: map (path: [name] ++ path) (getAttrPaths' value)) attrset
97+
);
98+
99+
/**
100+
getAttrPaths' but with checking
101+
102+
getAttrPaths :: { ... :: ?; _type ? :: string } -> [ attrPath ]
103+
*/
104+
getAttrPaths = set:
105+
map (
106+
path:
107+
if (last path) == 1
108+
then throw "Encountered mkIf value at ${showAttrPath (dropEnd 1 path)}"
109+
else path
110+
) (getAttrPaths' set);
111+
112+
pathToAttr = path: value:
113+
if (length path) > 0
114+
then {"${head path}" = pathToAttr (tail path) value;}
115+
else value;
116+
in {
117+
/**
118+
Take a deferredModule, and use it to determine what modules need loading
119+
resolveModulesFromLazyModule :: { modulesDir :: Path; deferredModule :: deferredModule; rumLib :: rumLib; extraModules ? :: [ module ] } -> [ module ]
120+
*/
121+
resolveModulesFromLazyModule = {
122+
modulesDir,
123+
deferredModule,
124+
rumLib,
125+
extraModules ? [],
126+
options,
127+
}: let
128+
moduleFileTree = resolveFileTreeRecursive modulesDir;
129+
/**
130+
To collect the paths of files to import we need to do a couple things:
131+
- ~~Figure out the attrPaths to the bottommost option declarations~~ 6 months later; what does this even mean??? ( 2(?) months after, I still dont know what I was yapping about )
132+
- Consider mkIf's
133+
- Make sure to filter (Filter *what* ??????)
134+
*/
135+
136+
/**
137+
Attempt to resolve all used config values without a matching option into filepaths pointing to modules
138+
139+
resolveLazyModules :: { config :: ?; options :: ? } -> moduleResolutionResult
140+
*/
141+
resolveLazyModules = {
142+
config,
143+
options,
144+
...
145+
}: let
146+
/**
147+
All config values that dont have a matching option (?)
148+
149+
freeformAttrPaths :: [ attrPath ]
150+
*/
151+
freeformAttrPaths = filter (
152+
path: let
153+
maybeOption = attrByPath path null options;
154+
in
155+
!(isAttrs maybeOption && maybeOption ? _type && maybeOption._type == "option")
156+
) (getAttrPaths config);
157+
158+
/**
159+
Recurse into each part of a path and try to resolve it to a file, returning null when unsucessful
160+
161+
resolvePathToModule' :: attrPath -> { ...: string } -> string | null
162+
*/
163+
resolvePathToModule' = path: filetree: let
164+
headElem = head path;
165+
in
166+
if hasAttr headElem filetree
167+
then let
168+
subtree = getAttr headElem filetree;
169+
in
170+
assert (isAttrs subtree);
171+
resolvePathToModule' (tail path) subtree
172+
else let
173+
fileHeadElem = headElem + ".nix";
174+
in
175+
if hasAttr fileHeadElem filetree
176+
then let
177+
file = getAttr fileHeadElem filetree;
178+
in
179+
assert isString file; file
180+
else if filetree ? "default.nix"
181+
then filetree."default.nix"
182+
else null;
183+
184+
/**
185+
Resolve an attrPath to a module file.
186+
Return format: { "attr.path.seperated.with.dots" = "file/or/null/if/file/doesn't/exist.nix"}
187+
188+
resolvePathToModule :: attrPath -> { ${attrPath} :: string | null }
189+
*/
190+
resolvePathToModule = path: {
191+
"${concatStringsSep "." path}" = resolvePathToModule' path moduleFileTree;
192+
};
193+
194+
/**
195+
All resolved and unresolved module files
196+
197+
allModules :: { ${attrPath} :: string | null }
198+
*/
199+
allModules = mergeAttrsList (map resolvePathToModule freeformAttrPaths);
200+
201+
/**
202+
Get it? because you filter for all items "where Value is [what]". e.g. all items where Value is String
203+
204+
whereValue :: (? -> bool) -> { ... :: ? } -> { ... :: ? }
205+
*/
206+
whereValue = isWhat: filterAttrs (_: isWhat);
207+
208+
out = {
209+
resolved = attrValues (whereValue isString allModules);
210+
unresolved = attrNames (whereValue isNull allModules);
211+
};
212+
in
213+
out;
214+
215+
/**
216+
iterate :: [ module ] -> { config :: { ... :: ? }; options :: { ... :: ? } }
217+
*/
218+
iterate = resolvedModules:
219+
evalModules {
220+
modules =
221+
resolvedModules
222+
++ [
223+
deferredModule
224+
(
225+
let
226+
opts = removeAttrs options ["_module"];
227+
in {
228+
_file = "${__curPos.file}:${builtins.toString __curPos.line}";
229+
options = opts;
230+
231+
config = {
232+
_module = {
233+
freeformType = lib.types.attrs;
234+
args = {inherit rumLib;};
235+
};
236+
};
237+
}
238+
)
239+
];
240+
};
241+
/**
242+
converge :: [ module ] -> moduleResolutionResult -> Int -> { config :: { ... :: ? }; unresolved :: [ string ] }
243+
*/
244+
converge = resolvedModules: prevAllModules: limit:
245+
if limit == 0
246+
then throw "Module evaluation did not converge after iteration limit"
247+
else let
248+
current = iterate prevAllModules.resolved;
249+
currentAllModules = resolveLazyModules current;
250+
in
251+
if (length currentAllModules.resolved) == 0
252+
then {
253+
config = current.config;
254+
inherit (currentAllModules) unresolved;
255+
} # Converged
256+
else converge (resolvedModules ++ currentAllModules.resolved) currentAllModules (limit - 1);
257+
258+
/**
259+
converged :: { config :: { ... :: ?}; unresolved :: [ string ] }
260+
*/
261+
converged = converge extraModules {resolved = [];} 20;
262+
263+
/**
264+
config :: { ... :: ? }
265+
*/
266+
config =
267+
if (length converged.unresolved) > 0
268+
then throw "Hjem-Rum: Couldn't find module(s) matching the following configuration value(s) \n ${converged.unresolved}"
269+
else converged.config;
270+
271+
/**
272+
`config`, filtered to only include values that match non-rum options. (i.e. hjem options)
273+
Yes, this basically is the same logic as for `freeformAtrrPaths`, except the filter inverted
274+
275+
finalConfigAttrPaths :: { ... :: ? }
276+
*/
277+
finalConfigAttrPaths = filter (
278+
path: let
279+
maybeOption = attrByPath path null options;
280+
in (isAttrs maybeOption && maybeOption ? _type && maybeOption._type == "option")
281+
) (getAttrPaths config);
282+
283+
finalConfig = fold recursiveUpdate {} (
284+
map (path: pathToAttr path (attrByPath path null config)) finalConfigAttrPaths
285+
);
286+
in
287+
finalConfig;
288+
}

modules/lib/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
attrsets = import ./attrsets {inherit lib;};
33
generators = import ./generators {inherit lib;};
44
types = import ./types {inherit lib;};
5+
modules = import ./black_magic.nix {inherit lib;};
56
}

0 commit comments

Comments
 (0)