my public nix modules
1{ self, lib, config, ... }: let
2 cfg = config.modularizer;
3in {
4 options.modularizer = {
5 paths = lib.mkOption {
6 type = lib.types.listOf lib.types.path;
7 default = [];
8 description = "List of paths to import";
9 };
10
11 modules = lib.mkOption {
12 # ./path/name/type.nix or ./path/name/type/default.nix
13 # --> becomes -->
14 # { "type" = { "name" = f: import ./path/name/type.nix; }; } or
15 # { "type" = { "name" = f: import ./path/name/type/default.nix; }; }
16 # ./path/ can be longer, like ./path/path/path/name/type.nix
17 # the recursive search will stop in any directory with a default.nix,
18 # allowing for either single .nix files for each type or an entire directory.
19 type = with lib.types; attrsOf (attrsOf (functionTo attrs));
20
21 internal = true;
22 readOnly = true;
23
24 default = let
25
26 inherit (builtins)
27 elemAt
28 length
29 isFunction
30 groupBy
31 mapAttrs
32 listToAttrs
33 readFileType
34 ;
35
36 inherit (lib)
37 concatMapAttrs
38 hasSuffix
39 removeSuffix
40 pathExists
41 splitString
42 attrsToList
43 concatMap
44 ;
45
46 importFromDirectoryRecursive =
47 directory:
48 let
49 processDir =
50 {
51 basePath,
52 localPath ? "", # should be emtpy or start with "/"
53 }:
54 let
55 fullPathDir = basePath + localPath;
56 in
57 concatMapAttrs (
58 name: type:
59 let
60 fullPath = fullPathDir + "/" + name;
61 isFile = type == "regular";
62 isDir = type == "directory";
63 isImportableDir =
64 isDir &&
65 pathExists (fullPath + "/" + "default.nix") &&
66 readFileType (fullPath + "/" + "default.nix") == "regular";
67 isSupported =
68 isFile || # regular file
69 (isDir && !isImportableDir) || # dir with no default.nix file
70 isImportableDir # dir where default.nix is a regular file
71 ;
72 in
73
74 # unknown type
75 if !isSupported then
76 throw ''
77 modulesFromDirectoryRecursive: Unsupported file type ${type} at path ${toString fullPath}
78 ''
79
80 # duplicate importable files with the same name,
81 # directory/default.nix and directory.nix both exist
82 else if
83 isImportableDir &&
84 pathExists (fullPath + ".nix")
85 then
86 throw ''
87 modulesFromDirectoryRecursive: Found both a directory ${fullPath} and file ${fullPath}/default.nix which conflict (same module name).
88 Suggested action: Delete the ${name}.nix file and move its contents into ${name}/default.nix or vice versa.
89 ''
90
91 # .nix file or directory with default.nix
92 else if
93 (isFile && hasSuffix ".nix" name) ||
94 isImportableDir
95 then
96 {
97 # FIXME: importing an empty file or otherwise a non-nix file will freeze execution...
98 "${localPath + "/" + (removeSuffix ".nix" name)}" = import fullPath;
99 }
100
101 # directory without default.nix
102 else if isDir && !isImportableDir then
103 processDir
104 {
105 inherit basePath;
106 localPath = localPath + "/" + name;
107 }
108
109 # non-nix file
110 else
111 { }
112 ) (builtins.readDir fullPathDir);
113 in processDir { basePath = directory; };
114
115 processImportedModule = m:
116 let
117 pathParts = splitString "/" m.name;
118 type = elemAt pathParts (length pathParts - 1);
119 # first element is an empty string because of the root slash
120 # [ "" "moduleName" ... "type" ]
121 name = if length pathParts >= 3 then elemAt pathParts 1 else "default";
122 value = if isFunction m.value then m.value else _: m.value;
123 in {
124 inherit type name value;
125 };
126
127 allDirs = map importFromDirectoryRecursive cfg.paths;
128 allImports = concatMap attrsToList allDirs;
129 allModules = map processImportedModule allImports;
130
131 byType = groupBy (e: e.type) allModules;
132 finalize = mapAttrs (_: v: listToAttrs v) byType;
133
134 in finalize;
135 };
136
137 };
138}