···11+# Easy Hosts
22+33+This is a nix flake module, this means that it is intended to be used alongside [flake-parts](https://flake.parts).
44+55+You can find some examples of how to use this module in the [examples](./examples) directory.
66+77+## Why use this?
88+99+We provide you with the following attributes `self'` and `inputs'` that can be used to make your configuration shorter going from writing `inputs.input-name.packages.${pkgs.system}.package-name` to `inputs'.input-name.packages.package-name`.
1010+1111+We also can auto construct your hosts based on your file structure. Whilst providing you with a nice api which will allow you to add more settings to your hosts at a later date or consume another flake-module that can work alongside this flake.
1212+1313+## Explaination of the module
1414+1515+- `easyHosts.autoConstruct`: If set to true, the module will automatically construct the hosts for you from the directory structure of `easyHosts.path`.
1616+1717+- `easyHosts.path`: The directory to where the hosts are stored, this *must* be set.
1818+1919+- `easyHosts.onlySystem`: If you only have 1 system type like `aarch64-darwin` then you can use this setting to prevent nesting your directories.
2020+2121+- `easyHosts.shared`: The shared options for all the hosts.
2222+ - `modules`: A list of modules that will be included in all the hosts.
2323+ - `specialArgs`: A list of special arguments that will be passed to all the hosts.
2424+2525+- `easyHosts.perClass`: This provides you with the `class` argument such that you can specify what classes get which modules.
2626+ - `modules`: A list of modules that will be included in all the hosts of the given class.
2727+ - `specialArgs`: A list of special arguments that will be passed to all the hosts of the given class.
2828+2929+- `easyHosts.hosts.<host>`: The options for the given host.
3030+ - `path`: the path to the host, this is not strictly needed if you have a flat directory called `hosts` or `systems`.
3131+ - `arch`: The architecture of the host.
3232+ - `modules`: A list of modules that will be included in the host.
3333+ - `class`: the class of the host, this can be one of [ "nixos", "darwin", "iso" ].
3434+ - `specialArgs`: A list of special arguments that will be passed to the host.
3535+ - `deployable`: this was added for people who may want to consume a deploy-rs or colonma flakeModule.
3636+3737+## Similar projects
3838+3939+- [ez-configs](https://github.com/ehllie/ez-configs)
4040+4141+## Real world examples
4242+4343+- [isabelroses/dotfiles](https://github.com/isabelroses/dotfiles)
···11+{
22+ lib,
33+ inputs,
44+ config,
55+ withSystem,
66+ ...
77+}:
88+let
99+ inherit (lib.options) mkOption literalExpression;
1010+ inherit (lib) types;
1111+1212+ inherit (import ./lib.nix { inherit lib inputs withSystem; })
1313+ constructSystem
1414+ mkHosts
1515+ buildHosts
1616+ ;
1717+1818+ cfg = config.easyHosts;
1919+in
2020+{
2121+ options = {
2222+ easyHosts = {
2323+ autoConstruct = lib.mkEnableOption "Automatically construct hosts";
2424+2525+ path = mkOption {
2626+ type = types.nullOr types.path;
2727+ default = null;
2828+ example = literalExpression "./hosts";
2929+ };
3030+3131+ onlySystem = mkOption {
3232+ type = types.nullOr types.str;
3333+ default = null;
3434+ example = literalExpression "aarch64-darwin";
3535+ };
3636+3737+ shared = {
3838+ modules = mkOption {
3939+ # we really expect a list of paths but i want to accept lists of lists of lists and so on
4040+ # since they will be flattened in the final function that applies the settings
4141+ type = types.listOf types.anything;
4242+ default = [ ];
4343+ };
4444+4545+ specialArgs = mkOption {
4646+ type = types.attrs;
4747+ default = { };
4848+ };
4949+ };
5050+5151+ perClass = mkOption {
5252+ default = _: {
5353+ modules = [ ];
5454+ specialArgs = { };
5555+ };
5656+5757+ type = types.functionTo (
5858+ types.submodule {
5959+ options = {
6060+ modules = mkOption {
6161+ type = types.listOf types.anything;
6262+ default = [ ];
6363+ };
6464+6565+ specialArgs = mkOption {
6666+ type = types.attrs;
6767+ default = { };
6868+ };
6969+ };
7070+ }
7171+ );
7272+ };
7373+7474+ hosts = mkOption {
7575+ default = { };
7676+ type = types.attrsOf (
7777+ types.submodule (
7878+ { name, ... }:
7979+ let
8080+ self = cfg.hosts.${name};
8181+ in
8282+ {
8383+ options = {
8484+ arch = mkOption {
8585+ type = types.str;
8686+ default = "x86_64";
8787+ };
8888+8989+ class = mkOption {
9090+ type = types.str;
9191+ default = "nixos";
9292+ };
9393+9494+ system = mkOption {
9595+ type = types.str;
9696+ default = constructSystem self.class self.arch;
9797+ };
9898+9999+ path = mkOption {
100100+ type = types.nullOr types.path;
101101+ default = null;
102102+ example = literalExpression "./hosts/myhost";
103103+ };
104104+105105+ deployable = mkOption {
106106+ type = types.bool;
107107+ default = false;
108108+ };
109109+110110+ modules = mkOption {
111111+ type = types.listOf types.anything;
112112+ default = [ ];
113113+ };
114114+115115+ specialArgs = mkOption {
116116+ type = types.attrs;
117117+ default = { };
118118+ };
119119+ };
120120+ }
121121+ )
122122+ );
123123+ };
124124+ };
125125+ };
126126+127127+ config = {
128128+ # if the user has made it such that they want the hosts to be constructed automatically
129129+ # i.e. from the file paths then we will do that
130130+ easyHosts.hosts = lib.mkIf cfg.autoConstruct (buildHosts cfg);
131131+132132+ flake = mkHosts cfg;
133133+ };
134134+}
+29
flake.nix
···11+{
22+ inputs = { };
33+44+ outputs = _: {
55+ flakeModule = ./flake-module.nix;
66+77+ templates = {
88+ multi = {
99+ path = ./templates/multi;
1010+ description = "A multi-system flake with auto construction enabled, but only using x86_64-linux.";
1111+ };
1212+1313+ multi-specialised = {
1414+ path = ./templates/multi-specialised;
1515+ description = "A multi-system flake with auto construction enabled, using the custom class system of easy-hosts";
1616+ };
1717+1818+ not-auto = {
1919+ path = ./templates/not-auto;
2020+ description = "A flake with auto construction disabled, using only the `easyHosts.hosts` attribute.";
2121+ };
2222+2323+ only = {
2424+ path = ./templates/only;
2525+ description = "A flake with auto construction enabled, with only one class and a more 'flat' structure.";
2626+ };
2727+ };
2828+ };
2929+}
+252
lib.nix
···11+{
22+ lib,
33+ inputs,
44+ withSystem,
55+ ...
66+}:
77+let
88+ inherit (inputs) self;
99+1010+ constructSystem =
1111+ target: arch:
1212+ if (target == "iso" || target == "nixos") then "${arch}-linux" else "${arch}-${target}";
1313+1414+ inherit (builtins)
1515+ readDir
1616+ elemAt
1717+ filter
1818+ pathExists
1919+ ;
2020+ inherit (lib.lists) optionals singleton flatten;
2121+ inherit (lib.attrsets)
2222+ recursiveUpdate
2323+ foldAttrs
2424+ attrValues
2525+ mapAttrs
2626+ filterAttrs
2727+ ;
2828+ inherit (lib.modules) mkDefault evalModules;
2929+3030+ /**
3131+ mkHost is a function that uses withSystem to give us inputs' and self'
3232+ it also assumes the the system type either nixos or darwin and uses the appropriate
3333+3434+ # Type
3535+3636+ ```
3737+ mkHost :: AttrSet -> AttrSet
3838+ ```
3939+4040+ # Example
4141+4242+ ```nix
4343+ mkHost {
4444+ name = "myhost";
4545+ path = "/path/to/host";
4646+ system = "x86_64-linux";
4747+ class = "nixos";
4848+ modules = [ ./module.nix ];
4949+ specialArgs = { foo = "bar"; };
5050+ }
5151+ ```
5252+ */
5353+ mkHost =
5454+ {
5555+ name,
5656+ path,
5757+ class ? "nixos",
5858+ system ? "x86_64-linux",
5959+ modules ? [ ],
6060+ specialArgs ? { },
6161+ ...
6262+ }:
6363+ withSystem system (
6464+ { self', inputs', ... }:
6565+ let
6666+ eval = evalModules {
6767+ # we use recursiveUpdate such that users can "override" the specialArgs
6868+ #
6969+ # This should only be used for special arguments that need to be evaluated
7070+ # when resolving module structure (like in imports).
7171+ specialArgs = recursiveUpdate {
7272+ # create the modulesPath based on the system, we need
7373+ modulesPath =
7474+ if class == "darwin" then "${inputs.darwin}/modules" else "${inputs.nixpkgs}/nixos/modules";
7575+7676+ # laying it out this way is completely arbitrary, however it looks nice i guess
7777+ inherit lib;
7878+ inherit self self';
7979+ inherit inputs inputs';
8080+ } specialArgs;
8181+8282+ # A nominal type for modules. When set and non-null, this adds a check to
8383+ # make sure that only compatible modules are imported.
8484+ class = if class == "iso" then "nixos" else class;
8585+8686+ modules = flatten [
8787+ # import our host system paths
8888+ (
8989+ if path != null then
9090+ path
9191+ else
9292+ (filter pathExists [
9393+ # if the previous path does not exist then we will try to import some paths with some assumptions
9494+ "${self}/hosts/${name}/default.nix"
9595+ "${self}/systems/${name}/default.nix"
9696+ ])
9797+ )
9898+9999+ # get an installer profile from nixpkgs to base the Isos off of
100100+ # this is useful because it makes things alot easier
101101+ (optionals (class == "iso") [
102102+ "${inputs.nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel.nix"
103103+ ])
104104+105105+ # we need to import the module list for our system
106106+ # this is either the nixos modules list provided by nixpkgs
107107+ # or the darwin modules list provided by nix darwin
108108+ (import (
109109+ if class == "darwin" then
110110+ "${inputs.darwin}/modules/module-list.nix"
111111+ else
112112+ "${inputs.nixpkgs}/nixos/modules/module-list.nix"
113113+ ))
114114+115115+ (singleton {
116116+ # TODO: learn what this means and why its needed to build the iso
117117+ _module.args.modules = [ ];
118118+119119+ # we set the systems hostname based on the host value
120120+ # which should be a string that is the hostname of the system
121121+ networking.hostName = name;
122122+123123+ nixpkgs = {
124124+ # you can also do this as `inherit system;` with the normal `lib.nixosSystem`
125125+ # however for evalModules this will not work, so we do this instead
126126+ hostPlatform = mkDefault system;
127127+128128+ # The path to the nixpkgs sources used to build the system.
129129+ # This is automatically set up to be the store path of the nixpkgs flake used to build
130130+ # the system if using lib.nixosSystem, and is otherwise null by default.
131131+ # so that means that we should set it to our nixpkgs flake output path
132132+ flake.source = inputs.nixpkgs.outPath;
133133+ };
134134+ })
135135+136136+ # if we are on darwin we need to import the nixpkgs source, its used in some
137137+ # modules, if this is not set then you will get an error
138138+ (optionals (class == "darwin") (singleton {
139139+ # without supplying an upstream nixpkgs source, nix-darwin will not be able to build
140140+ # and will complain and log an error demanding that you must set this value
141141+ nixpkgs.source = mkDefault inputs.nixpkgs;
142142+143143+ system = {
144144+ # i don't quite know why this is set but upstream does it so i will too
145145+ checks.verifyNixPath = false;
146146+147147+ # we use these values to keep track of what upstream revision we are on, this also
148148+ # prevents us from recreating docs for the same configuration build if nothing has changed
149149+ darwinVersionSuffix = ".${inputs.darwin.shortRev or inputs.darwin.dirtyShortRev or "dirty"}";
150150+ darwinRevision = inputs.darwin.rev or inputs.darwin.dirtyRev or "dirty";
151151+ };
152152+ }))
153153+154154+ # import any additional modules that the user has provided
155155+ modules
156156+ ];
157157+ };
158158+ in
159159+ if (class == "nixos" || class == "iso") then
160160+ { nixosConfigurations.${name} = eval; }
161161+ else
162162+ {
163163+ darwinConfigurations.${name} = eval // {
164164+ system = eval.config.system.build.toplevel;
165165+ };
166166+ }
167167+ );
168168+169169+ foldAttrsReccursive = builtins.foldl' (acc: attrs: recursiveUpdate acc attrs) { };
170170+171171+ mkHosts =
172172+ makeHostsConfig:
173173+ foldAttrs (host: acc: host // acc) { } (
174174+ attrValues (
175175+ mapAttrs (
176176+ name: cfg:
177177+ mkHost {
178178+ inherit name;
179179+180180+ inherit (cfg) class system path;
181181+182182+ # merging is handled later
183183+ modules = [
184184+ (cfg.modules or [ ])
185185+ (makeHostsConfig.shared.modules or [ ])
186186+ ((makeHostsConfig.perClass cfg.class).modules or [ ])
187187+ ];
188188+189189+ specialArgs = foldAttrsReccursive [
190190+ (cfg.specialArgs or { })
191191+ (makeHostsConfig.shared.specialArgs or { })
192192+ ((makeHostsConfig.perClass cfg.class).specialArgs or { })
193193+ ];
194194+ }
195195+ ) makeHostsConfig.hosts
196196+ )
197197+ );
198198+199199+ onlyDirs = filterAttrs (_: type: type == "directory");
200200+201201+ splitSystem =
202202+ system:
203203+ let
204204+ sp = builtins.split "-" system;
205205+ arch = elemAt sp 0;
206206+ class = if ((elemAt sp 2) == "linux") then "nixos" else elemAt sp 2;
207207+ in
208208+ {
209209+ inherit arch class;
210210+ };
211211+212212+ normaliseHosts =
213213+ cfg: hosts:
214214+ if (cfg.onlySystem == null) then
215215+ foldAttrs (acc: host: acc // host) { } (
216216+ attrValues (
217217+ mapAttrs (
218218+ system: hosts':
219219+ mapAttrs (name: _: {
220220+ inherit (splitSystem system) arch class;
221221+ path = "${cfg.path}/${system}/${name}";
222222+ }) hosts'
223223+ ) hosts
224224+ )
225225+ )
226226+ else
227227+ mapAttrs (host: _: {
228228+ inherit (splitSystem cfg.onlySystem) arch class;
229229+ path = "${cfg.path}/${host}";
230230+ }) hosts;
231231+232232+ buildHosts =
233233+ cfg:
234234+ let
235235+ hostsDir = readDir cfg.path;
236236+237237+ hosts =
238238+ if (cfg.onlySystem != null) then
239239+ hostsDir
240240+ else
241241+ mapAttrs (path: _: readDir "${cfg.path}/${path}") (onlyDirs hostsDir);
242242+ in
243243+ normaliseHosts cfg hosts;
244244+in
245245+{
246246+ inherit
247247+ constructSystem
248248+ mkHost
249249+ mkHosts
250250+ buildHosts
251251+ ;
252252+}