a flake module to ease creating and managing multiple hosts in your nix flake.
1{
2 lib,
3 inputs,
4 withSystem,
5}:
6let
7 inherit (inputs) self;
8
9 inherit (builtins) readDir;
10 inherit (lib)
11 elemAt
12 filter
13 pathExists
14 foldAttrs
15 pipe
16 optionals
17 singleton
18 concatLists
19 recursiveUpdate
20 attrValues
21 mapAttrs
22 filterAttrs
23 mkDefault
24 mergeAttrs
25 ;
26
27 /**
28 classToOS
29
30 # Arguments
31
32 - [class]: The class of the system. This is usually one of `nixos`, `darwin`, or `iso`.
33
34 # Type
35
36 ```
37 classToOS :: String -> String
38 ```
39
40 # Example
41
42 ```nix
43 classToOS "darwin"
44 => "darwin"
45 ```
46
47 ```nix
48 classToOS "nixos"
49 => "linux"
50 ```
51 */
52 classToOS = class: if (class == "darwin") then "darwin" else "linux";
53
54 /**
55 classToND
56
57 # Arguments
58
59 - [class]: The class of the system. This is usually one of `nixos`, `darwin`, or `iso`.
60
61 # Type
62
63 ```
64 classToND :: String -> String
65 ```
66
67 # Example
68
69 ```nix
70 classToND "darwin"
71 => "darwin"
72 ```
73
74 ```nix
75 classToND "iso"
76 => "nixos"
77 ```
78 */
79 classToND = class: if (class == "darwin") then "darwin" else "nixos";
80
81 /**
82 redefineClass
83
84 # Arguments
85
86 - [additionalClasses]: A set of additional classes to be used for the system.
87 - [class]: The class of the system. This is usually one of `nixos`, `darwin`, or `iso`.
88
89 # Type
90
91 ```
92 redefineClass :: AttrSet -> String -> String
93 ```
94
95 # Example
96
97 ```nix
98 redefineClass { rpi = "nixos"; } "linux"
99 => "nixos"
100 ```
101
102 ```nix
103 redefineClass { rpi = "nixos"; } "rpi"
104 => "nixos"
105 ```
106 */
107 redefineClass =
108 additionalClasses: class: ({ linux = "nixos"; } // additionalClasses).${class} or class;
109
110 /**
111 constructSystem
112
113 # Arguments
114
115 - [additionalClasses]: A set of additional classes to be used for the system.
116 - [arch]: The architecture of the system. This is usually one of `x86_64`, `aarch64`, or `armv7l`.
117 - [class]: The class of the system. This is usually one of `nixos`, `darwin`, or `iso`.
118
119 # Type
120
121 ```
122 constructSystem :: AttrSet -> String -> String -> String
123 ```
124
125 # Example
126
127 ```nix
128 constructSystem { rpi = "nixos"; } "x86_64" "rpi"
129 => "x86_64-linux"
130 ```
131
132 ```nix
133 constructSystem { rpi = "nixos"; } "x86_64" "linux"
134 => "x86_64-linux"
135 ```
136 */
137 constructSystem =
138 additionalClasses: arch: class:
139 let
140 class' = redefineClass additionalClasses class;
141 os = classToOS class';
142 in
143 "${arch}-${os}";
144
145 /**
146 splitSystem
147
148 # Arguments
149
150 - [system]: The system to be split. This is usually one of `x86_64-linux`, `aarch64-darwin`, or `armv7l-linux`.
151
152 # Type
153
154 ```
155 splitSystem :: String -> AttrSet
156 ```
157
158 # Example
159
160 ```nix
161 splitSystem "x86_64-linux"
162 => { arch = "x86_64"; class = "linux"; }
163 ```
164
165 ```nix
166 splitSystem "aarch64-darwin"
167 => { arch = "aarch64"; class = "darwin"; }
168 ```
169 */
170 splitSystem =
171 system:
172 let
173 sp = builtins.split "-" system;
174 arch = elemAt sp 0;
175 class = elemAt sp 2;
176 in
177 {
178 inherit arch class;
179 };
180
181 /**
182 mkHost is a function that uses withSystem to give us inputs' and self'
183 it also assumes the the system type either nixos or darwin and uses the appropriate
184
185 # Type
186
187 ```
188 mkHost :: AttrSet -> AttrSet
189 ```
190
191 # Example
192
193 ```nix
194 mkHost {
195 name = "myhost";
196 path = "/path/to/host";
197 system = "x86_64-linux";
198 class = "nixos";
199 modules = [ ./module.nix ];
200 specialArgs = { foo = "bar"; };
201 }
202 ```
203 */
204 mkHost =
205 {
206 name,
207 path,
208 # by the time we receive the argument here it can only be one of
209 # nixos, darwin, or iso. The redefineClass function should be used prior
210 # nixos, darwin. The redefineClass function should be used prior
211 class,
212 system,
213 nixpkgs,
214 nix-darwin,
215 modules ? [ ],
216 specialArgs ? { },
217 ...
218 }:
219 let
220 evalHost = if class == "darwin" then nix-darwin.lib.darwinSystem else nixpkgs.lib.nixosSystem;
221 in
222 evalHost {
223 # we use recursiveUpdate such that users can "override" the specialArgs
224 #
225 # This should only be used for special arguments that need to be evaluated
226 # when resolving module structure (like in imports).
227 specialArgs = recursiveUpdate {
228 inherit
229 # these are normal args that people expect to be passed,
230 # but we expect to be evaluated when resolving module structure
231 inputs
232
233 # even though self is just the same as `inputs.self`
234 # we still pass this as some people will use this
235 self
236 ;
237 } specialArgs;
238
239 modules = concatLists [
240 # import our host system paths
241 (
242 if path != null then
243 [ path ]
244 else
245 (filter pathExists [
246 # if the previous path does not exist then we will try to import some paths with some assumptions
247 "${self}/hosts/${name}/default.nix"
248 "${self}/systems/${name}/default.nix"
249 ])
250 )
251
252 # get an installer profile from nixpkgs to base the Isos off of
253 # this is useful because it makes things alot easier
254 (optionals (class == "iso") [
255 "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel.nix"
256 ])
257
258 # the next 3 singleton's are split up to make it easier to understand as they do things different things
259
260 # recall `specialArgs` would take be preferred when resolving module structure
261 # well this is how we do it use it for all args that don't need to rosolve module structure
262 (singleton {
263 key = "easy-hosts#specialArgs";
264 _file = "${__curPos.file}";
265
266 _module.args = withSystem system (
267 { self', inputs', ... }:
268 {
269 inherit self' inputs';
270 }
271 );
272 })
273
274 # here we make some basic assumptions about the system the person is using
275 # like the system type and the hostname
276 (singleton {
277 key = "easy-hosts#hostname";
278 _file = "${__curPos.file}";
279
280 # we set the systems hostname based on the host value
281 # which should be a string that is the hostname of the system
282 networking.hostName = mkDefault name;
283 })
284
285 (singleton {
286 key = "easy-hosts#nixpkgs";
287 _file = "${__curPos.file}";
288
289 nixpkgs = {
290 # you can also do this as `inherit system;` with the normal `lib.nixosSystem`
291 # however for evalModules this will not work, so we do this instead
292 hostPlatform = mkDefault system;
293
294 # The path to the nixpkgs sources used to build the system.
295 # This is automatically set up to be the store path of the nixpkgs flake used to build
296 # the system if using lib.nixosSystem, and is otherwise null by default.
297 # so that means that we should set it to our nixpkgs flake output path
298 flake.source = nixpkgs.outPath;
299 };
300 })
301
302 # if we are on darwin we need to import the nixpkgs source, its used in some
303 # modules, if this is not set then you will get an error
304 (optionals (class == "darwin") (singleton {
305 key = "easy-hosts#nixpkgs-darwin";
306 _file = "${__curPos.file}";
307
308 # without supplying an upstream nixpkgs source, nix-darwin will not be able to build
309 # and will complain and log an error demanding that you must set this value
310 nixpkgs.source = mkDefault nixpkgs;
311 }))
312
313 # import any additional modules that the user has provided
314 modules
315 ];
316 };
317
318 /**
319 toHostOutput
320
321 # Arguments
322
323 - [name]: The name of the host.
324 - [class]: The class of the host. This is usually one of `nixos`, `darwin`, or `iso`.
325 - [output]: The output of the host.
326
327 # Type
328
329 ```
330 toHostOutput :: AttrSet -> AttrSet
331 ```
332
333 # Example
334
335 ```nix
336 toHostOutput {
337 name = "myhost";
338 class = "nixos";
339 output = { };
340 }
341 => { nixosConfigurations.myhost = { }; }
342 ```
343 */
344 toHostOutput =
345 {
346 name,
347 class,
348 output,
349 }:
350 if ((classToND class) == "nixos") then
351 { nixosConfigurations.${name} = output; }
352 else
353 { darwinConfigurations.${name} = output; };
354
355 foldAttrsMerge = foldAttrs mergeAttrs { };
356 foldAttrsMergeRec = foldAttrs recursiveUpdate { };
357
358 /**
359 mkHosts is a function that takes a set of hosts and returns a set of host outputs.
360
361 # Arguments
362
363 - [easyHostsConfig]: The easy-hosts configuration.
364
365 # Type
366
367 ```
368 mkHosts :: AttrSet -> AttrSet
369 ```
370 */
371 mkHosts =
372 easyHostsConfig:
373 pipe easyHostsConfig.hosts [
374 (mapAttrs (
375 name: hostConfig:
376 let
377 # memoize the class and perClass values so we don't have to recompute them
378 sources = lib.flatten [
379 # modules and specialArgs from different sources combined
380 easyHostsConfig.shared
381 hostConfig
382 (builtins.map easyHostsConfig.perTag hostConfig.tags)
383 (easyHostsConfig.perClass hostConfig.class)
384 (easyHostsConfig.perArch hostConfig.arch)
385 ];
386 class = redefineClass easyHostsConfig.additionalClasses hostConfig.class;
387 in
388 toHostOutput {
389 inherit name class;
390
391 output = mkHost {
392 inherit name class;
393
394 inherit (hostConfig)
395 system
396 path
397 nixpkgs
398 nix-darwin
399 ;
400
401 modules = concatLists (builtins.map (x: x.modules) sources);
402 specialArgs = builtins.foldl' recursiveUpdate { } (builtins.map (x: x.specialArgs) sources);
403 };
404 }
405 ))
406
407 attrValues
408 foldAttrsMerge
409 ];
410
411 /**
412 normaliseHost
413
414 # Arguments
415
416 - [additionalClasses]: A set of additional classes to be used for the system.
417 - [system]: The system to be normalised. This is usually one of `x86_64-linux`, `aarch64-darwin`, or `armv7l-linux`.
418 - [path]: The path to the host.
419
420 # Type
421
422 ```
423 normaliseHost :: AttrSet -> String -> String -> AttrSet
424 ```
425
426 # Example
427
428 ```nix
429 normaliseHost { rpi = "nixos"; } "x86_64-linux" "/path/to/host"
430 => { arch = "x86_64"; class = "linux"; path = "/path/to/host"; system = "x86_64-linux"; }
431 ```
432 */
433 normaliseHost =
434 additionalClasses: system: path:
435 let
436 inherit (splitSystem system) arch class;
437 in
438 {
439 inherit arch class path;
440 system = constructSystem additionalClasses arch class;
441 };
442
443 /**
444 normaliseHosts is a function that takes a set of hosts and returns a set of normalised hosts.
445
446 # Arguments
447
448 - [cfg]: The easy-hosts configuration.
449 - [hosts]: The hosts to be normalised.
450
451 # Type
452
453 ```
454 normaliseHosts :: AttrSet -> AttrSet -> AttrSet
455 ```
456 */
457 normaliseHosts =
458 cfg: hosts:
459 if (cfg.onlySystem == null) then
460 pipe hosts [
461 (mapAttrs (
462 system:
463 mapAttrs (name: _: normaliseHost cfg.additionalClasses system "${cfg.path}/${system}/${name}")
464 ))
465
466 attrValues
467 foldAttrsMerge
468 ]
469 else
470 mapAttrs (name: _: normaliseHost cfg.additionalClasses cfg.onlySystem "${cfg.path}/${name}") hosts;
471
472 /**
473 buildHosts is a function that takes a configuration and returns a set of hosts.
474 It is used to build the hosts for the system.
475
476 # Arguments
477
478 - [cfg]: The easy-hosts configuration.
479
480 # Type
481
482 ```
483 buildHosts :: AttrSet -> AttrSet
484 ```
485 */
486 buildHosts =
487 cfg:
488 let
489 hostsDir = readDir cfg.path;
490
491 hosts =
492 if (cfg.onlySystem != null) then
493 hostsDir
494 else
495 mapAttrs (path: _: readDir "${cfg.path}/${path}") (
496 filterAttrs (_: type: type == "directory") hostsDir
497 );
498 in
499 normaliseHosts cfg hosts;
500in
501{
502 inherit
503 constructSystem
504 mkHost
505 mkHosts
506 buildHosts
507 ;
508}