diff --git a/lib/flake/default.nix b/lib/flake/default.nix index e4af712..979b3e2 100644 --- a/lib/flake/default.nix +++ b/lib/flake/default.nix @@ -72,15 +72,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 +135,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/home/default.nix b/lib/home/default.nix new file mode 100644 index 0000000..a956e39 --- /dev/null +++ b/lib/home/default.nix @@ -0,0 +1,214 @@ +{ core-inputs, user-inputs, snowfall-lib }: + +let + inherit (core-inputs.nixpkgs.lib) assertMsg foldl head tail concatMap optionalAttrs mkIf filterAttrs mapAttrs' mkMerge mapAttrsToList; + + 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; + }); + + 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-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; + + 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; + })) + ]; + + extraSpecialArgs = specialArgs // args.specialArgs; + }); + }; + + 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 = builtins.attrValues 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-home-system-modules = users: + let + created-users = create-homes users; + 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; + # pkgs = user-inputs.self.pkgs.${system}.nixpkgs; + + 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 = tail created-user.modules; + user-name = created-user.specialArgs.user; + in + args@{ config + , pkgs + , host ? "" + , ... + }: + let + host-matches = created-user.specialArgs.host == host; + + wrapped-user-module = home-args: + let + modified-args = args // { + inherit (created-user.specialArgs) user; + }; + user-module-result = (import user-module (home-args // modified-args)) // { + _file = user-module; + }; + in + user-module-result; + in + { + _file = "virtual:snowfallorg/home/user/${name}"; + + imports = + if snowfall-lib.system.is-darwin created-user.system then + [ ../../modules/darwin/home/default.nix ] + else + [ ../../modules/nixos/home/default.nix ]; + + config = mkIf host-matches { + home-manager = { + users.${user-name} = wrapped-user-module args; + sharedModules = other-modules; + }; + }; + } + ) + (builtins.attrNames created-users); + in + [ extra-special-args-module ] ++ 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/module/default.nix b/lib/module/default.nix index cb10acf..7b647cb 100644 --- a/lib/module/default.nix +++ b/lib/module/default.nix @@ -16,7 +16,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 +36,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/system/default.nix b/lib/system/default.nix index b2a5d1e..9df24f9 100644 --- a/lib/system/default.nix +++ b/lib/system/default.nix @@ -5,21 +5,25 @@ 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,7 +89,7 @@ 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; @@ -94,12 +98,13 @@ in }; }); 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" ]) // { + specialArgs = args.specialArgs // { + format = "darwin"; + }; + }); linux-system-builder = args: core-inputs.nixpkgs.lib.nixosSystem (args // { @@ -158,17 +163,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 +191,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/modules/darwin/home/default.nix b/modules/darwin/home/default.nix new file mode 100644 index 0000000..e9cd0a8 --- /dev/null +++ b/modules/darwin/home/default.nix @@ -0,0 +1,33 @@ +{ lib, config, ... }: + +let + inherit (lib) types mkOption mkMerge mapAttrsToList; + + home-submodule = { name, ... }: { + options = { + proxy = mkOption { + type = types.attrs; + default = { }; + description = "Configuration to be proxied to the home-manager configuration for `home-manager.users.`."; + }; + }; + }; + + cfg = config.snowfallorg; +in +{ + options.snowfallorg = { + home = mkOption { + type = types.attrsOf (types.submodule home-submodule); + default = { }; + description = "Options for configuring home environments."; + }; + }; + + config = mkMerge + (mapAttrsToList + (name: value: { + home-manager.users.${name} = value.proxy; + }) + (cfg.home)); +} diff --git a/modules/home/os/default.nix b/modules/home/os/default.nix new file mode 100644 index 0000000..2da065f --- /dev/null +++ b/modules/home/os/default.nix @@ -0,0 +1,28 @@ +{ lib, osConfig ? { }, ... }: + +let + inherit (lib) types mkOption; + + home-submodule = { name, ... }: { + options = { + proxy = mkOption { + type = types.attrs; + default = { }; + description = "Configuration to be proxied to the home-manager configuration for `home-manager.users.`."; + }; + }; + }; +in +{ + options.snowfallorg = { + home = mkOption { + type = types.attrsOf (types.submodule home-submodule); + default = { }; + description = "Options for configuring home environments."; + }; + }; + + config = { + # snowfallorg.home = osConfig.snowfallorg.home or { }; + }; +} diff --git a/modules/nixos/home/default.nix b/modules/nixos/home/default.nix new file mode 100644 index 0000000..e9cd0a8 --- /dev/null +++ b/modules/nixos/home/default.nix @@ -0,0 +1,33 @@ +{ lib, config, ... }: + +let + inherit (lib) types mkOption mkMerge mapAttrsToList; + + home-submodule = { name, ... }: { + options = { + proxy = mkOption { + type = types.attrs; + default = { }; + description = "Configuration to be proxied to the home-manager configuration for `home-manager.users.`."; + }; + }; + }; + + cfg = config.snowfallorg; +in +{ + options.snowfallorg = { + home = mkOption { + type = types.attrsOf (types.submodule home-submodule); + default = { }; + description = "Options for configuring home environments."; + }; + }; + + config = mkMerge + (mapAttrsToList + (name: value: { + home-manager.users.${name} = value.proxy; + }) + (cfg.home)); +}