diff --git a/README.md b/README.md index 6ea7ca0..7769dcd 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,13 @@ cd config 2. Create a new flake with one of the templates from [@snowfallorg/templates](https://github.com/snowfallorg/templates). -| Name | Description | -| --------- | ------------------------------------------------- | -| `system` | A NixOS system and modules ready to modify. | -| `package` | A Nix Flake that exports packages and an overlay. | -| `module` | A Nix Flake that exports NixOS modules. | -| `lib` | A Nix Flake that exports a custom `lib` | +| Name | Description | +| --------- | ---------------------------------------------------- | +| `system` | A NixOS system and modules ready to modify. | +| `package` | A Nix Flake that exports packages and an overlay. | +| `module` | A Nix Flake that exports NixOS modules. | +| `lib` | A Nix Flake that exports a custom `lib` | +| `empty` | A basic Nix Flake for you to customize from scratch. | ```bash # For example, to use the system template. @@ -195,12 +196,20 @@ snowfall-root/ │ ├─ modules/ (optional modules) │ │ -│ │ Any (nestable) directory name. The name of the directory will be the -│ │ name of the module. -│ └─ **/ +│ │ A directory named after the `platform` type that will be used for modules within. +│ │ +│ │ Supported platforms are: +│ │ - nixos +│ │ - darwin +│ │ - home +│ └─ / │ │ -│ │ A NixOS module. -│ └─ default.nix +│ │ Any (nestable) directory name. The name of the directory will be the +│ │ name of the module. +│ └─ **/ +│ │ +│ │ A NixOS module. +│ └─ default.nix │ ├─ overlays/ (optional overlays) │ │ @@ -246,6 +255,37 @@ snowfall-root/ │ │ │ │ A NixOS module for your system's configuration. │ └─ default.nix +│ +├─ homes/ (optional homes configurations) +│ │ +│ │ A directory named after the `home` type that will be used for all homes within. +│ │ +│ │ The architecture is any supported architecture of NixPkgs, for example: +│ │ - x86_64 +│ │ - aarch64 +│ │ - i686 +│ │ +│ │ The format is any supported NixPkgs format *or* a format provided by either nix-darwin +│ │ or nixos-generators. However, in order to build systems with nix-darwin or nixos-generators, +│ │ you must add `darwin` and `nixos-generators` inputs to your flake respectively. Here +│ │ are some example formats: +│ │ - linux +│ │ - darwin +│ │ - iso +│ │ - install-iso +│ │ - do +│ │ - vmware +│ │ +│ │ With the architecture and format together (joined by a hyphen), you get the name of the +│ │ directory for the home type. +│ └─ -/ +│ │ +│ │ A directory that contains a single home's configuration. The directory name +│ │ will be the name of the home. +│ └─ / +│ │ +│ │ A NixOS module for your home's configuration. +│ └─ default.nix ``` #### Default Flake @@ -526,6 +566,48 @@ type. See the following table for a list of supported formats from NixOS Generat | vm-nogui | Same as vm, but without a GUI | | vmware | VMWare image (VMDK) | +#### Home Manager + +Snowfall Lib supports configuring [Home Manager](https://github.com/nix-community/home-manager) +for both standalone use and for use as a module with NixOS or nix-darwin. To use this feature, +your flake must include `home-manager` as an input. + +```nix +{ + description = "My Flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; + + snowfall-lib = { + url = "github:snowfallorg/lib"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # In order to use Home Manager. + home-manager = { + url = "github:nix-community/home-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs: + # This is an example and in your actual flake you can use `snowfall-lib.mkFlake` + # directly unless you explicitly need a feature of `lib`. + let + lib = inputs.snowfall-lib.mkLib { + # You must pass in both your flake's inputs and the root directory of + # your flake. + inherit inputs; + src = ./.; + }; + in + # No additional configuration is required to use this feature, you only + # have to add home-manager to your flake inputs. + lib.mkFlake { }; +} +``` + ### `lib.snowfall.flake` Helpers related to Nix flakes. @@ -1162,6 +1244,24 @@ Result: "iso" ``` +#### `lib.snowfall.system.get-inferred-system-name` + +Get the name of a system based on its file path. + +Type: `Path -> String` + +Usage: + +```nix +get-inferred-system-name "/systems/my-system/default.nix" +``` + +Result: + +```nix +"my-system" +``` + #### `lib.snowfall.system.get-target-systems-metadata` Get structured data about all systems for a given target. @@ -1261,7 +1361,7 @@ Type: `Attrs -> Attrs` Usage: ```nix -create-systems { hosts.my-host.specialArgs.x = true; modules = [ my-shared-module ]; } +create-systems { hosts.my-host.specialArgs.x = true; modules.nixos = [ my-shared-module ]; } ``` Result: @@ -1270,6 +1370,98 @@ Result: { my-host = ; } ``` +### `lib.snowfall.home` + +#### `lib.snowfall.home.split-user-and-host` + +Get the user and host from a combined string. + +Type: `String -> Attrs` + +Usage: + +```nix +split-user-and-host "myuser@myhost" +``` + +Result: + +```nix +{ user = "myuser"; host = "myhost"; } +``` + +#### `lib.snowfall.home.create-home` + +Create a home. + +Type: `Attrs -> Attrs` + +Usage: + +```nix +create-home { path = ./homes/my-home; } +``` + +Result: + +```nix + +``` + +#### `lib.snowfall.home.create-homes` + +Create all available homes. + +Type: `Attrs -> Attrs` + +Usage: + +```nix +create-homes { users."my-user@my-system".specialArgs.x = true; modules = [ my-shared-module ]; } +``` + +Result: + +```nix +{ "my-user@my-system" = ; } +``` + +#### `lib.snowfall.home.get-target-homes-metadata` + +Get structured data about all homes for a given target. + +Type: `String -> [Attrs]` + +Usage: + +```nix +get-target-homes-metadata ./homes +``` + +Result: + +```nix +[ { system = "x86_64-linux"; name = "my-home"; path = "/homes/x86_64-linux/my-home";} ] +``` + +#### `lib.snowfall.home.create-home-system-modules` + +Create system modules for home-manager integration. + +Type: `Attrs -> [Module]` + +Usage: + +```nix +create-home-system-modules { users."my-user@my-system".specialArgs.x = true; modules = [ my-shared-module ]; } +``` + +Result: + +```nix +[Module] +``` + ### `lib.snowfall.package` Utilities for working with flake packages. diff --git a/flake.nix b/flake.nix index 49ba302..e0deb3d 100644 --- a/flake.nix +++ b/flake.nix @@ -27,10 +27,10 @@ # A convenience wrapper to create the library and then call `lib.mkFlake`. # Usage: mkFlake { inherit inputs; src = ./.; ... } # result: - mkFlake = flake-and-lib-options@{ inputs, src, ... }: + mkFlake = flake-and-lib-options@{ inputs, src, snowfall ? { }, ... }: let lib = mkLib { - inherit inputs src; + inherit inputs src snowfall; }; flake-options = builtins.removeAttrs flake-and-lib-options [ "inputs" "src" ]; in @@ -38,5 +38,17 @@ in { inherit mkLib mkFlake; + + nixosModules = { + user = ./modules/nixos/user/default.nix; + }; + + darwinModules = { + user = ./modules/darwin/user/default.nix; + }; + + homeModules = { + user = ./modules/home/user/default.nix; + }; }; } diff --git a/lib/attrs/default.nix b/lib/attrs/default.nix index 9896757..c7a171f 100644 --- a/lib/attrs/default.nix +++ b/lib/attrs/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/lib/default.nix b/lib/default.nix index ee1c6e8..05f0c3d 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -6,6 +6,11 @@ core-inputs: user-options: let + raw-snowfall-config = user-options.snowfall or { }; + snowfall-config = raw-snowfall-config // { + root = raw-snowfall-config.root or user-options.src; + }; + user-inputs = user-options.inputs // { src = user-options.src; }; inherit (core-inputs.nixpkgs.lib) assertMsg fix filterAttrs mergeAttrs fold recursiveUpdate callPackageWith; @@ -60,7 +65,7 @@ let snowfall-lib = fix (snowfall-lib: let attrs = { - inherit snowfall-lib core-inputs user-inputs; + inherit snowfall-lib snowfall-config core-inputs user-inputs; }; libs = builtins.map (dir: import "${snowfall-lib-root}/${dir}" attrs) diff --git a/lib/flake/default.nix b/lib/flake/default.nix index e4af712..15de25a 100644 --- a/lib/flake/default.nix +++ b/lib/flake/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let @@ -47,6 +48,7 @@ rec { "templates" "package-namespace" "alias" + "snowfall" ]; # Transform an attribute set of inputs into an attribute set where @@ -72,15 +74,30 @@ rec { package-namespace = full-flake-options.package-namespace or "internal"; custom-flake-options = flake.without-snowfall-options full-flake-options; alias = full-flake-options.alias or { }; - systems = snowfall-lib.system.create-systems (full-flake-options.systems or { }); - hosts = snowfall-lib.attrs.merge-shallow [ (full-flake-options.systems.hosts or { }) systems ]; + homes = snowfall-lib.home.create-homes (full-flake-options.homes or { }); + systems = snowfall-lib.system.create-systems { + systems = (full-flake-options.systems or { }); + homes = (full-flake-options.homes or { }); + }; + hosts = snowfall-lib.attrs.merge-shallow [ (full-flake-options.systems.hosts or { }) systems homes ]; templates = snowfall-lib.template.create-templates { overrides = (full-flake-options.templates or { }); alias = alias.templates or { }; }; - modules = snowfall-lib.module.create-modules { - overrides = (full-flake-options.modules or { }); - alias = alias.modules or { }; + nixos-modules = snowfall-lib.module.create-modules { + src = snowfall-lib.fs.get-snowfall-file "modules/nixos"; + overrides = (full-flake-options.modules.nixos or { }); + alias = alias.modules.nixos or { }; + }; + darwin-modules = snowfall-lib.module.create-modules { + src = snowfall-lib.fs.get-snowfall-file "modules/darwin"; + overrides = (full-flake-options.modules.darwin or { }); + alias = alias.modules.darwin or { }; + }; + home-modules = snowfall-lib.module.create-modules { + src = snowfall-lib.fs.get-snowfall-file "modules/home"; + overrides = (full-flake-options.modules.home or { }); + alias = alias.modules.home or { }; }; overlays = snowfall-lib.overlay.create-overlays { inherit package-namespace; @@ -120,7 +137,9 @@ rec { lib = snowfall-lib.internal.user-lib; inputs = snowfall-lib.flake.without-src user-inputs; - nixosModules = modules; + nixosModules = nixos-modules; + darwinModules = darwin-modules; + homeModules = home-modules; channelsConfig = full-flake-options.channels-config or { }; diff --git a/lib/fp/default.nix b/lib/fp/default.nix index 78da0ee..63e0826 100644 --- a/lib/fp/default.nix +++ b/lib/fp/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/lib/fs/default.nix b/lib/fs/default.nix index 3791a81..54cbe64 100644 --- a/lib/fs/default.nix +++ b/lib/fs/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let @@ -31,7 +32,7 @@ in # Type: Path -> Path # Usage: get-snowfall-file "systems" # result: "/user-source/snowfall-dir/systems" - get-snowfall-file = path: "${user-inputs.snowfall.root or user-inputs.src}/${path}"; + get-snowfall-file = path: "${snowfall-config.root}/${path}"; # Get a file path relative to the this flake. # Type: Path -> Path diff --git a/lib/home/default.nix b/lib/home/default.nix new file mode 100644 index 0000000..c2ea41b --- /dev/null +++ b/lib/home/default.nix @@ -0,0 +1,310 @@ +{ core-inputs +, user-inputs +, snowfall-lib +, snowfall-config +}: + +let + inherit (core-inputs.nixpkgs.lib) + assertMsg + foldl + head + tail + concatMap + optionalAttrs + optional + mkIf + filterAttrs + mapAttrs' + mkMerge + mapAttrsToList + optionals + mkDefault + mkAliasDefinitions + mkAliasAndWrapDefinitions + mkOption + types; + + user-homes-root = snowfall-lib.fs.get-snowfall-file "homes"; + user-modules-root = snowfall-lib.fs.get-snowfall-file "modules"; +in +{ + home = rec { + # Modules in home-manager expect `hm` to be available directly on `lib` itself. + home-lib = snowfall-lib.internal.system-lib.extend (final: prev: + # @NOTE(jakehamilton): This order is important, this library's extend and other utilities must write + # _over_ the original `system-lib`. + snowfall-lib.internal.system-lib + // prev + // { + hm = snowfall-lib.internal.system-lib.home-manager.hm; + }); + + # Get the user and host from a combined string. + # Type: String -> Attrs + # Usage: split-user-and-host "myuser@myhost" + # result: { user = "myuser"; host = "myhost"; } + split-user-and-host = target: + let + raw-name-parts = builtins.split "@" target; + name-parts = builtins.filter builtins.isString raw-name-parts; + + user = builtins.elemAt name-parts 0; + host = + if builtins.length name-parts > 1 then + builtins.elemAt name-parts 1 + else + ""; + in + { + inherit user host; + }; + + + # Create a home. + # Type: Attrs -> Attrs + # Usage: create-home { path = ./homes/my-home; } + # result: + create-home = + { path + , name ? builtins.unsafeDiscardStringContext (snowfall-lib.system.get-inferred-system-name path) + , modules ? [ ] + , specialArgs ? { } + , channelName ? "nixpkgs" + , system ? "x86_64-linux" + }: + let + user-metadata = split-user-and-host name; + + # @NOTE(jakehamilton): home-manager has trouble with `pkgs` recursion if it isn't passed in here. + pkgs = user-inputs.self.pkgs.${system}.${channelName} // { lib = home-lib; }; + lib = home-lib; + in + assert assertMsg (user-inputs ? home-manager) "In order to create home-manager configurations, you must include `home-manager` as a flake input."; + assert assertMsg (user-metadata.host != "") "Snowfall Lib homes must be named with the format: user@system"; + { + inherit channelName system; + + output = "homeConfigurations"; + + modules = [ + path + ../../modules/home/user/default.nix + ] ++ modules; + + specialArgs = { + inherit name; + inherit (user-metadata) user host; + + format = "home"; + + inputs = snowfall-lib.flake.without-src user-inputs; + + # @NOTE(jakehamilton): home-manager has trouble with `pkgs` recursion if it isn't passed in here. + inherit pkgs lib; + }; + + builder = args: + user-inputs.home-manager.lib.homeManagerConfiguration + ((builtins.removeAttrs args [ "system" "specialArgs" ]) // { + inherit pkgs lib; + + modules = args.modules ++ [ + (module-args: import ./nix-registry-module.nix (module-args // { + inherit user-inputs core-inputs; + })) + ({ + snowfallorg.user.name = mkDefault user-metadata.user; + }) + ]; + + extraSpecialArgs = specialArgs // args.specialArgs; + }); + }; + + # Get structured data about all homes for a given target. + # Type: String -> [Attrs] + # Usage: get-target-homes-metadata ./homes + # result: [ { system = "x86_64-linux"; name = "my-home"; path = "/homes/x86_64-linux/my-home";} ] + get-target-homes-metadata = target: + let + homes = snowfall-lib.fs.get-directories target; + existing-homes = builtins.filter (home: builtins.pathExists "${home}/default.nix") homes; + create-home-metadata = path: { + path = "${path}/default.nix"; + # We are building flake outputs based on file contents. Nix doesn't like this + # so we have to explicitly discard the string's path context to allow us to + # use the name as a variable. + name = builtins.unsafeDiscardStringContext (builtins.baseNameOf path); + # We are building flake outputs based on file contents. Nix doesn't like this + # so we have to explicitly discard the string's path context to allow us to + # use the name as a variable. + system = builtins.unsafeDiscardStringContext (builtins.baseNameOf target); + }; + home-configurations = builtins.map create-home-metadata existing-homes; + in + home-configurations; + + # Create all available homes. + # Type: Attrs -> Attrs + # Usage: create-homes { users."my-user@my-system".specialArgs.x = true; modules = [ my-shared-module ]; } + # result: { "my-user@my-system" = ; } + create-homes = homes: + let + targets = snowfall-lib.fs.get-directories user-homes-root; + target-homes-metadata = concatMap get-target-homes-metadata targets; + + user-home-modules = snowfall-lib.module.create-modules { + src = "${user-modules-root}/home"; + }; + + user-home-modules-list = mapAttrsToList + (module-path: module: args@{ pkgs, ... }: (module args) // { + _file = "${user-homes-root}/${module-path}/default.nix"; + }) + user-home-modules; + + create-home' = home-metadata: + let + inherit (home-metadata) name; + overrides = homes.users.${name} or { }; + in + { + "${name}" = create-home (overrides // home-metadata // { + modules = user-home-modules-list ++ (homes.users.${name}.modules or [ ]) ++ (homes.modules or [ ]); + }); + }; + + created-homes = foldl (homes: home-metadata: homes // (create-home' home-metadata)) { } target-homes-metadata; + in + created-homes; + + # Create system modules for home-manager integration. + # Type: Attrs -> [Module] + # Usage: create-home-system-modules { users."my-user@my-system".specialArgs.x = true; modules = [ my-shared-module ]; } + # result: [Module] + create-home-system-modules = users: + let + created-users = create-homes users; + user-home-modules = snowfall-lib.module.create-modules { + src = "${user-modules-root}/home"; + }; + + shared-modules = mapAttrsToList + (module-path: module: { + _file = "${user-modules-root}/home/${module-path}/default.nix"; + + config = { + home-manager.sharedModules = [ module ]; + }; + }) + user-home-modules; + + snowfall-user-home-module = { + _file = "virtual:snowfallorg/modules/home/user/default.nix"; + + config = { + home-manager.sharedModules = [ + ../../modules/home/user/default.nix + ]; + }; + }; + + extra-special-args-module = + args@{ config + , pkgs + , system ? pkgs.system + , target ? system + , format ? "home" + , host ? "" + , virtual ? (snowfall-lib.system.is-virtual target) + , systems ? { } + , ... + }: + { + _file = "virtual:snowfallorg/home/extra-special-args"; + + config = { + home-manager.extraSpecialArgs = { + inherit system target format virtual systems host; + + lib = home-lib; + + inputs = snowfall-lib.flake.without-src user-inputs; + }; + }; + }; + + system-modules = builtins.map + (name: + let + created-user = created-users.${name}; + user-module = head created-user.modules; + other-modules = users.users.${name}.modules or [ ]; + user-name = created-user.specialArgs.user; + in + args@{ config + , options + , pkgs + , host ? "" + , ... + }: + let + host-matches = created-user.specialArgs.host == host; + + # @NOTE(jakehamilton): To conform to the config structure of home-manager, we have to + # remap the options coming from `snowfallorg.user..home.config` since `mkAliasDefinitions` + # does not let us target options within a submodule. + wrap-user-options = user-option: + if (user-option ? "_type") && user-option._type == "merge" then + user-option // { + contents = builtins.map + (merge-entry: + merge-entry.${user-name}.home.config or { } + ) + user-option.contents; + } + else + (builtins.trace '' + ============= + Snowfall Lib: + Option value for `snowfallorg.user.${user-name}` was not detected to be merged. + + Please report the issue on GitHub with a link to your configuration so we can debug the problem: + https://github.com/snowfallorg/lib/issues/new + ============= + '') + user-option; + in + { + _file = "virtual:snowfallorg/home/user/${name}"; + + config = mkIf host-matches { + # Initialize user information. + snowfallorg.user.${user-name}.home.config = { + snowfallorg.user = { + enable = true; + name = mkDefault user-name; + }; + }; + + home-manager = { + users.${user-name} = mkAliasAndWrapDefinitions wrap-user-options options.snowfallorg.user; + + # sharedModules = other-modules ++ optional config.snowfallorg.user.${user-name}.home.enable wrapped-user-module; + sharedModules = other-modules ++ optional config.snowfallorg.user.${user-name}.home.enable user-module; + }; + }; + } + ) + (builtins.attrNames created-users); + in + [ + extra-special-args-module + snowfall-user-home-module + ] + ++ (users.modules or [ ]) + ++ shared-modules + ++ system-modules; + }; +} diff --git a/lib/home/nix-registry-module.nix b/lib/home/nix-registry-module.nix new file mode 100644 index 0000000..c15f9e2 --- /dev/null +++ b/lib/home/nix-registry-module.nix @@ -0,0 +1,11 @@ +# This code is adapted from flake-utils-plus: +# https://github.com/gytis-ivaskevicius/flake-utils-plus/blob/2bf0f91643c2e5ae38c1b26893ac2927ac9bd82a/lib/options.nix +{ lib, config, user-inputs, core-inputs, ... }: + +{ + disabledModules = [ + # The module from flake-utils-plus only works on NixOS and nix-darwin. For home-manager + # to build, this module needs to be disabled. + "${core-inputs.flake-utils-plus}/lib/options.nix" + ]; +} diff --git a/lib/internal/default.nix b/lib/internal/default.nix index dfa4a78..40aa8b3 100644 --- a/lib/internal/default.nix +++ b/lib/internal/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/lib/module/default.nix b/lib/module/default.nix index cb10acf..6ef087a 100644 --- a/lib/module/default.nix +++ b/lib/module/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let @@ -16,7 +17,7 @@ in # Usage: create-modules { src = ./my-modules; overrides = { inherit another-module; }; alias = { default = "another-module" }; } # result: { another-module = ...; my-module = ...; default = ...; } create-modules = - { src ? user-modules-root + { src ? "${user-modules-root}/nixos" , overrides ? { } , alias ? { } }: @@ -36,7 +37,9 @@ in modules-metadata = builtins.map create-module-metadata user-modules; merge-modules = modules: metadata: modules // { - ${metadata.name} = args: + # @NOTE(jakehamilton): home-manager *requires* modules to specify named arguments or it will not + # pass values in. For this reason we must specify things like `pkgs` as a named attribute. + ${metadata.name} = args@{ pkgs, ... }: let system = args.system or args.pkgs.system; target = args.target or system; diff --git a/lib/overlay/default.nix b/lib/overlay/default.nix index cc8dccd..be2ea5d 100644 --- a/lib/overlay/default.nix +++ b/lib/overlay/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/lib/package/default.nix b/lib/package/default.nix index 3968ecd..b77b51c 100644 --- a/lib/package/default.nix +++ b/lib/package/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/lib/path/default.nix b/lib/path/default.nix index 216f194..602f42f 100644 --- a/lib/path/default.nix +++ b/lib/path/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/lib/shell/default.nix b/lib/shell/default.nix index aecd0e2..4eb5aec 100644 --- a/lib/shell/default.nix +++ b/lib/shell/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/lib/system/default.nix b/lib/system/default.nix index b2a5d1e..6d9bfa5 100644 --- a/lib/system/default.nix +++ b/lib/system/default.nix @@ -1,25 +1,30 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let inherit (builtins) dirOf baseNameOf; - inherit (core-inputs.nixpkgs.lib) assertMsg fix hasInfix concatMap foldl; + inherit (core-inputs.nixpkgs.lib) assertMsg fix hasInfix concatMap foldl optionals singleton; virtual-systems = import ./virtual-systems.nix; user-systems-root = snowfall-lib.fs.get-snowfall-file "systems"; user-modules-root = snowfall-lib.fs.get-snowfall-file "modules"; - - get-inferred-system-name = path: - if snowfall-lib.path.has-file-extension "nix" path then - snowfall-lib.path.get-parent-directory path - else - baseNameOf path; in { system = rec { + # Get the name of a system based on its file path. + # Type: Path -> String + # Usage: get-inferred-system-name "/systems/my-system/default.nix" + # result: "my-system" + get-inferred-system-name = path: + if snowfall-lib.path.has-file-extension "nix" path then + snowfall-lib.path.get-parent-directory path + else + baseNameOf path; + # Check whether a named system is macOS. # Type: String -> Bool # Usage: is-darwin "x86_64-linux" @@ -85,27 +90,37 @@ in let virtual-system-type = get-virtual-system-type target; virtual-system-builder = args: - assert (assertMsg (user-inputs ? nixos-generators) "In order to create virtual systems, you must include `nixos-generators` as a flake input."); + assert assertMsg (user-inputs ? nixos-generators) "In order to create virtual systems, you must include `nixos-generators` as a flake input."; user-inputs.nixos-generators.nixosGenerate (args // { format = virtual-system-type; specialArgs = args.specialArgs // { format = virtual-system-type; }; + modules = args.modules ++ [ + ../../modules/nixos/user/default.nix + ]; }); darwin-system-builder = args: - assert (assertMsg (user-inputs ? darwin) "In order to create virtual systems, you must include `darwin` as a flake input."); - user-inputs.darwin.lib.darwinSystem ((builtins.removeAttrs args [ "system" ]) // { - specialArgs = args.specialArgs // { - format = "darwin"; - }; - }); + assert assertMsg (user-inputs ? darwin) "In order to create virtual systems, you must include `darwin` as a flake input."; + user-inputs.darwin.lib.darwinSystem + ((builtins.removeAttrs args [ "system" "modules" ]) // { + specialArgs = args.specialArgs // { + format = "darwin"; + }; + modules = args.modules ++ [ + ../../modules/darwin/user/default.nix + ]; + }); linux-system-builder = args: core-inputs.nixpkgs.lib.nixosSystem (args // { specialArgs = args.specialArgs // { format = "linux"; }; + modules = args.modules ++ [ + ../../modules/nixos/user/default.nix + ]; }); in if virtual-system-type != "" then @@ -158,17 +173,26 @@ in , builder ? get-system-builder target , output ? get-system-output target , systems ? { } + , homes ? { } }: let lib = snowfall-lib.internal.system-lib; + home-system-modules = snowfall-lib.home.create-home-system-modules homes; + home-manager-module = + if is-darwin system then + user-inputs.home-manager.darwinModules.home-manager + else + user-inputs.home-manager.nixosModules.home-manager; + home-manager-modules = [ home-manager-module ] ++ home-system-modules; in { inherit channelName system builder output; - modules = [ path ] ++ modules; + modules = [ path ] ++ modules ++ (optionals (user-inputs ? home-manager) home-manager-modules); specialArgs = specialArgs // { - inherit target system name systems lib; + inherit target system systems lib; + host = name; virtual = (get-virtual-system-type target) != ""; inputs = snowfall-lib.flake.without-src user-inputs; @@ -177,21 +201,41 @@ in # Create all available systems. # Type: Attrs -> Attrs - # Usage: create-systems { hosts.my-host.specialArgs.x = true; modules = [ my-shared-module ]; } + # Usage: create-systems { hosts.my-host.specialArgs.x = true; modules.nixos = [ my-shared-module ]; } # result: { my-host = ; } - create-systems = systems: + create-systems = { systems ? { }, homes ? { } }: let targets = snowfall-lib.fs.get-directories user-systems-root; target-systems-metadata = concatMap get-target-systems-metadata targets; - user-modules = snowfall-lib.fs.get-default-nix-files-recursive user-modules-root; + user-nixos-modules = snowfall-lib.module.create-modules { + src = "${user-modules-root}/nixos"; + }; + user-darwin-modules = snowfall-lib.module.create-modules { + src = "${user-modules-root}/darwin"; + }; + nixos-modules = systems.modules.nixos or [ ]; + darwin-modules = systems.modules.darwin or [ ]; + create-system' = created-systems: system-metadata: let overrides = systems.hosts.${system-metadata.name} or { }; + user-modules = + if is-darwin system-metadata.target then + user-darwin-modules + else + user-nixos-modules; + user-modules-list = builtins.attrValues user-modules; + system-modules = + if is-darwin system-metadata.target then + darwin-modules + else + nixos-modules; in { ${system-metadata.name} = create-system (overrides // system-metadata // { systems = created-systems; - modules = user-modules ++ (overrides.modules or [ ]) ++ (systems.modules or [ ]); + modules = user-modules-list ++ (overrides.modules or [ ]) ++ system-modules; + inherit homes; }); }; created-systems = fix (created-systems: diff --git a/lib/template/default.nix b/lib/template/default.nix index 3e04626..6527c3a 100644 --- a/lib/template/default.nix +++ b/lib/template/default.nix @@ -1,6 +1,7 @@ { core-inputs , user-inputs , snowfall-lib +, snowfall-config }: let diff --git a/modules/darwin/user/default.nix b/modules/darwin/user/default.nix new file mode 100644 index 0000000..28f0931 --- /dev/null +++ b/modules/darwin/user/default.nix @@ -0,0 +1,82 @@ +{ pkgs, lib, options, config, inputs, ... }: + +let + inherit (lib) types mkOption mkDefault foldl optionalAttrs; + + cfg = config.snowfallorg; + + user-names = builtins.attrNames cfg.user; + + create-system-users = system-users: name: + let + user = cfg.user.${name}; + in + system-users // (optionalAttrs user.create { + ${name} = { + home = mkDefault user.home.path; + isHidden = mkDefault false; + }; + }); +in +{ + options.snowfallorg = { + user = mkOption { + description = "User configuration."; + default = { }; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + create = mkOption { + description = "Whether to create the user automatically."; + type = types.bool; + default = true; + }; + + home = { + enable = mkOption { + type = types.bool; + default = true; + }; + + path = mkOption { + type = types.str; + default = "/Users/${name}"; + }; + + config = mkOption { + # HM-compatible options taken from: + # https://github.com/nix-community/home-manager/blob/0ee5ab611dc1fbb5180bd7d88d2aeb7841a4d179/nixos/common.nix#L14 + type = types.submoduleWith { + specialArgs = { + osConfig = config; + modulesPath = "${inputs.home-manager}/modules"; + } // config.home-manager.extraSpecialArgs; + modules = [ + ({ lib, modulesPath, ... }: { + imports = import "${modulesPath}/modules.nix" { + inherit pkgs lib; + useNixpkgsModule = !config.home-manager.useGlobalPkgs; + }; + + config = { + submoduleSupport.enable = true; + submoduleSupport.externalPackageInstall = cfg.useUserPackages; + + home.username = config.users.users.${name}.name; + home.homeDirectory = config.users.users.${name}.home; + + nix.package = config.nix.package; + }; + }) + ] ++ config.home-manager.sharedModules; + }; + }; + }; + }; + })); + }; + }; + + config = { + users.users = (foldl create-system-users { } (user-names)); + }; +} diff --git a/modules/home/user/default.nix b/modules/home/user/default.nix new file mode 100644 index 0000000..7c7c93a --- /dev/null +++ b/modules/home/user/default.nix @@ -0,0 +1,52 @@ +inputs@{ pkgs, lib, options, config, ... }: + +let + inherit (lib) types mkOption mkIf mkDefault; + + cfg = config.snowfallorg; + + # @NOTE(jakehamilton): The module system chokes if it finds `osConfig` named in the module arguments + # when being used in standalone home-manager. To remedy this, we have to refer to the arguments set directly. + os-user-home = inputs.osConfig.users.users.${cfg.name}.home or null; + + has-user-name = (cfg.user.name or null) != null; + + default-home-directory = + if (os-user-home != null) then + os-user-home + else if pkgs.stdenv.isDarwin then + "/Users/${cfg.user.name}" + else + "/home/${cfg.user.name}"; +in +{ + options.snowfallorg = { + user = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to configure the user."; + }; + + name = mkOption { + type = types.str; + description = "The user's name."; + }; + + home = { + directory = mkOption { + type = types.str; + description = "The user's home directory."; + default = default-home-directory; + }; + }; + }; + }; + + config = mkIf cfg.user.enable { + home = { + username = mkIf has-user-name (mkDefault cfg.user.name); + homeDirectory = mkIf has-user-name (mkDefault cfg.user.home.directory); + }; + }; +} diff --git a/modules/nixos/user/default.nix b/modules/nixos/user/default.nix new file mode 100644 index 0000000..757997d --- /dev/null +++ b/modules/nixos/user/default.nix @@ -0,0 +1,95 @@ +{ pkgs, lib, options, config, inputs, ... }: + +let + inherit (lib) types mkOption mkDefault foldl optionalAttrs optional; + + cfg = config.snowfallorg; + + user-names = builtins.attrNames cfg.user; + + create-system-users = system-users: name: + let + user = cfg.user.${name}; + in + system-users // (optionalAttrs user.create { + ${name} = { + isNormalUser = mkDefault true; + + name = mkDefault name; + + home = mkDefault user.home.path; + group = mkDefault "users"; + + extraGroups = optional user.admin "wheel"; + }; + }); + +in +{ + options.snowfallorg = { + user = mkOption { + description = "User configuration."; + default = { }; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + create = mkOption { + description = "Whether to create the user automatically."; + type = types.bool; + default = true; + }; + + admin = mkOption { + description = "Whether the user should be added to the wheel group."; + type = types.bool; + default = true; + }; + + home = { + enable = mkOption { + type = types.bool; + default = true; + }; + + path = mkOption { + type = types.str; + default = "/home/${name}"; + }; + + config = mkOption { + # HM-compatible options taken from: + # https://github.com/nix-community/home-manager/blob/0ee5ab611dc1fbb5180bd7d88d2aeb7841a4d179/nixos/common.nix#L14 + type = types.submoduleWith { + specialArgs = { + osConfig = config; + modulesPath = "${inputs.home-manager}/modules"; + } // config.home-manager.extraSpecialArgs; + modules = [ + ({ lib, modulesPath, ... }: { + imports = import "${modulesPath}/modules.nix" { + inherit pkgs lib; + useNixpkgsModule = !config.home-manager.useGlobalPkgs; + }; + + config = { + submoduleSupport.enable = true; + submoduleSupport.externalPackageInstall = cfg.useUserPackages; + + home.username = config.users.users.${name}.name; + home.homeDirectory = config.users.users.${name}.home; + + nix.package = config.nix.package; + }; + }) + ] ++ config.home-manager.sharedModules; + }; + }; + }; + }; + })); + }; + }; + + config = { + users.users = (foldl (create-system-users) { } (user-names)); + }; +}