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