diff --git a/.gitignore b/.gitignore index 07c6d80..b261cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,8 @@ !/hosts/ +!/libs/ + !/modules/ + +!/tests/ diff --git a/flake.lock b/flake.lock index faca085..200067f 100644 --- a/flake.lock +++ b/flake.lock @@ -570,6 +570,27 @@ "type": "github" } }, + "nix-darwin": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783744694, + "narHash": "sha256-2cp6N3rrwnGYLTx9l6N+NI+kwrCWxvJUbj5WJhvB29A=", + "owner": "nix-darwin", + "repo": "nix-darwin", + "rev": "c3e90c89649b07d1a96e4b9dd6cd0d6e44b91a74", + "type": "github" + }, + "original": { + "owner": "nix-darwin", + "ref": "nix-darwin-26.05", + "repo": "nix-darwin", + "type": "github" + } + }, "nix-index-database": { "inputs": { "nixpkgs": [ @@ -962,6 +983,7 @@ "lanzaboote": "lanzaboote", "llm-agents": "llm-agents", "niri-flake": "niri-flake", + "nix-darwin": "nix-darwin", "nix-index-database": "nix-index-database", "nixos-hardware": "nixos-hardware", "nixos-wsl": "nixos-wsl", diff --git a/flake.nix b/flake.nix index e2a3541..5ca0dd0 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,11 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + nix-darwin = { + url = "github:nix-darwin/nix-darwin/nix-darwin-26.05"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # Hardware / Platform nixos-hardware.url = "github:NixOS/nixos-hardware/master"; nixos-wsl.url = "github:nix-community/NixOS-WSL"; diff --git a/flake/default.nix b/flake/default.nix index ade85a1..d14de03 100644 --- a/flake/default.nix +++ b/flake/default.nix @@ -2,5 +2,6 @@ imports = [ ./formatter.nix ./git-hooks.nix + ./registry.nix ]; } diff --git a/flake/registry.nix b/flake/registry.nix new file mode 100644 index 0000000..064836a --- /dev/null +++ b/flake/registry.nix @@ -0,0 +1,42 @@ +{ + inputs, + lib, + ... +}: +let + dotfilesLib = import ../libs { + inherit inputs lib; + root = ../.; + }; + hostSpecsPath = ../hosts/default.nix; + hostSpecs = + if builtins.pathExists hostSpecsPath then + let + value = import hostSpecsPath; + in + if builtins.isFunction value then + value ( + builtins.intersectAttrs (builtins.functionArgs value) { + inherit inputs lib; + } + ) + else + value + else + { }; + configurations = dotfilesLib.hosts.mkConfigurations hostSpecs; +in +{ + flake = { + inherit (configurations) darwinConfigurations nixosConfigurations; + lib = dotfilesLib; + }; + + perSystem = + { pkgs, ... }: + { + checks.registry = import ../tests/registry.nix { + inherit inputs lib pkgs; + }; + }; +} diff --git a/libs/default.nix b/libs/default.nix new file mode 100644 index 0000000..2528af7 --- /dev/null +++ b/libs/default.nix @@ -0,0 +1,17 @@ +{ + inputs, + lib ? inputs.nixpkgs.lib, + root, +}: +let + registry = import ./registry.nix { + inherit inputs lib; + modulesRoot = root + "/modules"; + }; + hosts = import ./hosts.nix { + inherit inputs lib registry; + }; +in +{ + inherit hosts registry; +} diff --git a/libs/hosts.nix b/libs/hosts.nix new file mode 100644 index 0000000..d69bfc1 --- /dev/null +++ b/libs/hosts.nix @@ -0,0 +1,168 @@ +{ + inputs, + lib, + registry, +}: +let + ensure = + condition: message: value: + if condition then value else throw "host registry: ${message}"; + + isLinux = system: lib.hasSuffix "-linux" system; + isDarwin = system: lib.hasSuffix "-darwin" system; + + hostFile = + spec: name: + let + path = spec.path + "/${name}"; + in + if builtins.pathExists path then path else null; + + selectedUnits = + spec: + [ "users.${spec.user}" ] + ++ map (name: "profiles.${name}") (spec.profiles or [ ]) + ++ map (name: "applications.${name}") (spec.applications or [ ]) + ++ (spec.units or [ ]); + + validateSpec = + name: spec: + ensure (builtins.isAttrs spec) "${name}: host specification must be an attribute set" ( + ensure (spec ? system && builtins.isString spec.system) "${name}: system is required" ( + ensure (isLinux spec.system || isDarwin spec.system) + "${name}: unsupported system '${spec.system}'; expected a Linux NixOS or Darwin system" + ( + ensure (spec ? user && builtins.isString spec.user && spec.user != "") "${name}: user is required" ( + ensure (spec ? path && builtins.pathExists spec.path) + "${name}: path must name an existing host directory" + ( + ensure + (lib.all + (field: builtins.isList (spec.${field} or [ ]) && lib.all builtins.isString (spec.${field} or [ ])) + [ + "profiles" + "applications" + "units" + ] + ) + "${name}: profiles, applications, and units must be lists of strings" + (ensure (builtins.isBool (spec.homeManager or true)) "${name}: homeManager must be a boolean" spec) + ) + ) + ) + ) + ); + + mkSpecialArgs = name: spec: { + inherit inputs registry; + inherit (spec) system; + hostName = name; + primaryUser = spec.user; + }; + + mkHomeManagerModule = + name: spec: selected: + let + homePath = hostFile spec "home.nix"; + homeModules = [ + (registry.mkModule { class = "home"; }) + (registry.mkSelectionModule selected) + ] + ++ lib.optional (homePath != null) homePath; + in + { + imports = [ inputs.home-manager.nixosModules.home-manager ]; + + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + extraSpecialArgs = mkSpecialArgs name spec; + users.${spec.user}.imports = homeModules; + }; + }; + + mkDarwinHomeManagerModule = + name: spec: selected: + let + homePath = hostFile spec "home.nix"; + homeModules = [ + (registry.mkModule { class = "home"; }) + (registry.mkSelectionModule selected) + ] + ++ lib.optional (homePath != null) homePath; + in + { + imports = [ inputs.home-manager.darwinModules.home-manager ]; + + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + extraSpecialArgs = mkSpecialArgs name spec; + users.${spec.user}.imports = homeModules; + }; + }; + + mkNixos = + name: rawSpec: + let + spec = validateSpec name rawSpec; + selected = registry.validateUnitIds (selectedUnits spec); + nixosPath = hostFile spec "nixos.nix"; + modules = [ + (registry.mkModule { class = "nixos"; }) + (registry.mkSelectionModule selected) + { networking.hostName = lib.mkDefault name; } + ] + ++ lib.optional (spec.homeManager or true) (mkHomeManagerModule name spec selected) + ++ lib.optional (nixosPath != null) nixosPath; + in + inputs.nixpkgs.lib.nixosSystem { + inherit (spec) system; + specialArgs = mkSpecialArgs name spec; + inherit modules; + }; + + mkDarwin = + name: rawSpec: + let + spec = validateSpec name rawSpec; + selected = registry.validateUnitIds (selectedUnits spec); + darwinPath = hostFile spec "darwin.nix"; + modules = [ + (registry.mkModule { class = "darwin"; }) + (registry.mkSelectionModule selected) + { networking.hostName = lib.mkDefault name; } + ] + ++ lib.optional (spec.homeManager or true) (mkDarwinHomeManagerModule name spec selected) + ++ lib.optional (darwinPath != null) darwinPath; + in + ensure (inputs ? nix-darwin) "${name}: the nix-darwin input is required" ( + inputs.nix-darwin.lib.darwinSystem { + inherit (spec) system; + specialArgs = mkSpecialArgs name spec; + inherit modules; + } + ); + + mkConfigurations = + hostSpecs: + let + validated = lib.mapAttrs validateSpec hostSpecs; + in + { + nixosConfigurations = lib.mapAttrs mkNixos ( + lib.filterAttrs (_: spec: isLinux spec.system) validated + ); + darwinConfigurations = lib.mapAttrs mkDarwin ( + lib.filterAttrs (_: spec: isDarwin spec.system) validated + ); + }; +in +{ + inherit + mkConfigurations + mkDarwin + mkNixos + selectedUnits + ; +} diff --git a/libs/registry.nix b/libs/registry.nix new file mode 100644 index 0000000..d573ec1 --- /dev/null +++ b/libs/registry.nix @@ -0,0 +1,324 @@ +{ + inputs, + lib, + modulesRoot, +}: +let + reservedFiles = { + common = "common.nix"; + nixos = "nixos.nix"; + darwin = "darwin.nix"; + home = "home.nix"; + meta = "meta.nix"; + }; + + isFile = kind: kind == "regular" || kind == "symlink"; + + ensure = + condition: message: value: + if condition then value else throw "unit registry: ${message}"; + + callWithAvailableArgs = + value: availableArgs: + if builtins.isFunction value then + value (builtins.intersectAttrs (builtins.functionArgs value) availableArgs) + else + value; + + pathFor = + relativePath: + if relativePath == [ ] then + modulesRoot + else + modulesRoot + "/${lib.concatStringsSep "/" relativePath}"; + + entryIsFile = entries: name: builtins.hasAttr name entries && isFile entries.${name}; + + normalizeMeta = + unit: + let + metaPath = unit.fragments.meta; + importedValue = + if metaPath == null then + { } + else + callWithAvailableArgs (import metaPath) { + inherit inputs lib unit; + }; + imported = + ensure (builtins.isAttrs importedValue) "${unit.id}: meta.nix must return an attribute set" + importedValue; + allowedKeys = [ + "description" + "includes" + "imports" + ]; + unknownKeys = lib.filter (name: !(builtins.elem name allowedKeys)) (builtins.attrNames imported); + description = imported.description or null; + includes = imported.includes or [ ]; + imports = imported.imports or { }; + allowedImportKeys = [ + "nixos" + "darwin" + "home" + ]; + unknownImportKeys = + if builtins.isAttrs imports then + lib.filter (name: !(builtins.elem name allowedImportKeys)) (builtins.attrNames imports) + else + [ ]; + normalized = { + inherit description includes; + imports = { + nixos = imports.nixos or [ ]; + darwin = imports.darwin or [ ]; + home = imports.home or [ ]; + }; + }; + in + ensure (unknownKeys == [ ]) + "${unit.id}: meta.nix has unsupported keys: ${lib.concatStringsSep ", " unknownKeys}" + ( + ensure (description == null || builtins.isString description) + "${unit.id}: meta.description must be a string" + ( + ensure (builtins.isList includes && lib.all builtins.isString includes) + "${unit.id}: meta.includes must be a list of fully qualified unit IDs" + ( + ensure (lib.unique includes == includes) "${unit.id}: meta.includes contains duplicate unit IDs" ( + ensure (builtins.isAttrs imports) "${unit.id}: meta.imports must be an attribute set" ( + ensure (unknownImportKeys == [ ]) + "${unit.id}: meta.imports has unsupported classes: ${lib.concatStringsSep ", " unknownImportKeys}" + ( + ensure (lib.all builtins.isList [ + normalized.imports.nixos + normalized.imports.darwin + normalized.imports.home + ]) "${unit.id}: every meta.imports. value must be a list" normalized + ) + ) + ) + ) + ) + ); + + makeUnit = + relativePath: entries: + let + directory = pathFor relativePath; + id = lib.concatStringsSep "." relativePath; + fragments = lib.mapAttrs ( + _class: fileName: if entryIsFile entries fileName then directory + "/${fileName}" else null + ) reservedFiles; + baseUnit = { + inherit + id + directory + fragments + relativePath + ; + optionPath = [ "my" ] ++ relativePath ++ [ "enable" ]; + kind = builtins.head relativePath; + name = lib.last relativePath; + } + // + lib.optionalAttrs (builtins.length relativePath > 2 && builtins.head relativePath == "profiles") + { + group = builtins.elemAt relativePath 1; + }; + in + ensure (relativePath != [ ]) "the modules root cannot itself be a unit" ( + ensure (lib.all (component: component != "" && !(lib.hasInfix "." component)) relativePath) + "${id}: path components must be non-empty and must not contain dots" + (baseUnit // { meta = normalizeMeta baseUnit; }) + ); + + walk = + relativePath: + let + directory = pathFor relativePath; + entries = builtins.readDir directory; + hasReservedFile = lib.any (fileName: entryIsFile entries fileName) ( + builtins.attrValues reservedFiles + ); + childDirectories = lib.filter (name: entries.${name} == "directory") (builtins.attrNames entries); + current = lib.optional hasReservedFile (makeUnit relativePath entries); + children = lib.concatMap (name: walk (relativePath ++ [ name ])) childDirectories; + in + current ++ children; + + discoveredUnits = + ensure (builtins.pathExists modulesRoot) "modules root does not exist: ${toString modulesRoot}" + (walk [ ]); + unitsById = builtins.listToAttrs (map (unit: lib.nameValuePair unit.id unit) discoveredUnits); + + dependencyValidation = lib.foldl' ( + valid: unit: + lib.foldl' ( + inner: includedId: + if builtins.hasAttr includedId unitsById then + inner + else + throw "unit registry: ${unit.id} includes missing unit '${includedId}'" + ) valid unit.meta.includes + ) true discoveredUnits; + + units = builtins.seq dependencyValidation unitsById; + unitIds = builtins.attrNames units; + + getUnit = + id: if builtins.hasAttr id units then units.${id} else throw "unit registry: unknown unit '${id}'"; + + validateUnitIds = + ids: + ensure ( + builtins.isList ids && lib.all builtins.isString ids + ) "selected units must be a list of strings" (map (id: builtins.seq (getUnit id) id) ids); + + optionDefinitions = lib.foldl' lib.recursiveUpdate { } ( + map ( + unit: + lib.setAttrByPath unit.optionPath ( + lib.mkOption { + type = lib.types.bool; + default = false; + description = + if unit.meta.description == null then + "Whether to enable the ${unit.id} unit." + else + "Whether to enable ${unit.meta.description}."; + } + ) + ) discoveredUnits + ); + + enabled = config: unit: lib.getAttrFromPath unit.optionPath config; + + enableUnit = id: lib.setAttrByPath (getUnit id).optionPath true; + + includeConfig = + config: unit: lib.mkIf (enabled config unit) (lib.mkMerge (map enableUnit unit.meta.includes)); + + fragmentClasses = { + nixos = [ + "common" + "nixos" + ]; + darwin = [ + "common" + "darwin" + ]; + home = [ "home" ]; + }; + + applyFragment = + { + config, + fragmentPath, + options, + specialArgs, + unit, + }: + let + fragment = import fragmentPath; + directArgs = specialArgs // { + inherit + config + lib + options + specialArgs + unit + ; + }; + fragmentArgSpec = builtins.functionArgs fragment; + fragmentArgs = builtins.listToAttrs ( + lib.concatMap ( + name: + if builtins.hasAttr name directArgs then + [ (lib.nameValuePair name directArgs.${name}) ] + else if fragmentArgSpec.${name} then + [ ] + else + [ (lib.nameValuePair name config._module.args.${name}) ] + ) (builtins.attrNames fragmentArgSpec) + ); + resultValue = if builtins.isFunction fragment then fragment fragmentArgs else fragment; + result = + ensure (builtins.isAttrs resultValue) + "${unit.id}: ${builtins.baseNameOf fragmentPath} must return an attribute set" + resultValue; + forbiddenKeys = lib.filter (name: builtins.hasAttr name result) [ + "imports" + "options" + "config" + ]; + in + ensure (forbiddenKeys == [ ]) + "${unit.id}: ${builtins.baseNameOf fragmentPath} is a configuration fragment and cannot define top-level ${lib.concatStringsSep ", " forbiddenKeys}" + result; + + externalImports = class: lib.concatMap (unit: unit.meta.imports.${class}) discoveredUnits; + + mkModule = + { class }: + ensure (builtins.hasAttr class fragmentClasses) "unsupported module class '${class}'" ( + builtins.seq dependencyValidation ( + { + config, + lib, + options, + specialArgs, + ... + }: + let + fragmentConfigs = lib.concatMap ( + unit: + lib.filter (value: value != null) ( + map ( + fragmentClass: + let + fragmentPath = unit.fragments.${fragmentClass}; + in + if fragmentPath == null then + null + else + lib.mkIf (enabled config unit) (applyFragment { + inherit + config + fragmentPath + options + specialArgs + unit + ; + }) + ) fragmentClasses.${class} + ) + ) discoveredUnits; + in + { + imports = externalImports class; + options = optionDefinitions; + config = lib.mkMerge ((map (includeConfig config) discoveredUnits) ++ fragmentConfigs); + } + ) + ); + + mkSelectionModule = + selectedIds: + let + checkedIds = validateUnitIds (lib.unique selectedIds); + in + { + config = lib.mkMerge (map enableUnit checkedIds); + }; +in +{ + inherit + getUnit + mkModule + mkSelectionModule + unitIds + units + validateUnitIds + ; +} diff --git a/tests/fixtures/registry-invalid-dependency/modules/applications/broken/meta.nix b/tests/fixtures/registry-invalid-dependency/modules/applications/broken/meta.nix new file mode 100644 index 0000000..6349450 --- /dev/null +++ b/tests/fixtures/registry-invalid-dependency/modules/applications/broken/meta.nix @@ -0,0 +1,3 @@ +{ + includes = [ "applications.missing" ]; +} diff --git a/tests/fixtures/registry-invalid-fragment/modules/applications/broken/home.nix b/tests/fixtures/registry-invalid-fragment/modules/applications/broken/home.nix new file mode 100644 index 0000000..e140d9d --- /dev/null +++ b/tests/fixtures/registry-invalid-fragment/modules/applications/broken/home.nix @@ -0,0 +1,3 @@ +{ + imports = [ ]; +} diff --git a/tests/fixtures/registry/hosts/darwin.nix b/tests/fixtures/registry/hosts/darwin.nix new file mode 100644 index 0000000..d7493a0 --- /dev/null +++ b/tests/fixtures/registry/hosts/darwin.nix @@ -0,0 +1,3 @@ +{ + system.stateVersion = 6; +} diff --git a/tests/fixtures/registry/hosts/nixos.nix b/tests/fixtures/registry/hosts/nixos.nix new file mode 100644 index 0000000..3df8577 --- /dev/null +++ b/tests/fixtures/registry/hosts/nixos.nix @@ -0,0 +1,9 @@ +{ + fileSystems."/" = { + device = "/dev/null"; + fsType = "ext4"; + }; + + boot.loader.grub.enable = false; + system.stateVersion = "26.05"; +} diff --git a/tests/fixtures/registry/modules/applications/alpha/common.nix b/tests/fixtures/registry/modules/applications/alpha/common.nix new file mode 100644 index 0000000..998e720 --- /dev/null +++ b/tests/fixtures/registry/modules/applications/alpha/common.nix @@ -0,0 +1,7 @@ +{ + marker ? "host", + ... +}: +{ + test.systemValues = [ "common:${marker}" ]; +} diff --git a/tests/fixtures/registry/modules/applications/alpha/darwin.nix b/tests/fixtures/registry/modules/applications/alpha/darwin.nix new file mode 100644 index 0000000..c7fe75c --- /dev/null +++ b/tests/fixtures/registry/modules/applications/alpha/darwin.nix @@ -0,0 +1,3 @@ +{ + test.systemValues = [ "darwin" ]; +} diff --git a/tests/fixtures/registry/modules/applications/alpha/home.nix b/tests/fixtures/registry/modules/applications/alpha/home.nix new file mode 100644 index 0000000..f65f95a --- /dev/null +++ b/tests/fixtures/registry/modules/applications/alpha/home.nix @@ -0,0 +1,3 @@ +{ + test.homeValues = [ "home" ]; +} diff --git a/tests/fixtures/registry/modules/applications/alpha/meta.nix b/tests/fixtures/registry/modules/applications/alpha/meta.nix new file mode 100644 index 0000000..f5c2f6c --- /dev/null +++ b/tests/fixtures/registry/modules/applications/alpha/meta.nix @@ -0,0 +1,39 @@ +{ lib, ... }: +{ + description = "alpha test application"; + + includes = [ "services.nested" ]; + + imports.nixos = [ + { + options.test = { + external = lib.mkOption { + type = lib.types.str; + default = "external-default"; + }; + systemValues = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + } + ]; + + imports.darwin = [ + { + options.test.systemValues = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + } + ]; + + imports.home = [ + { + options.test.homeValues = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + } + ]; +} diff --git a/tests/fixtures/registry/modules/applications/alpha/nixos.nix b/tests/fixtures/registry/modules/applications/alpha/nixos.nix new file mode 100644 index 0000000..395eecc --- /dev/null +++ b/tests/fixtures/registry/modules/applications/alpha/nixos.nix @@ -0,0 +1,4 @@ +{ + test.systemValues = [ "nixos" ]; + test.external = "set-by-nixos-fragment"; +} diff --git a/tests/fixtures/registry/modules/applications/beta/home.nix b/tests/fixtures/registry/modules/applications/beta/home.nix new file mode 100644 index 0000000..11a981f --- /dev/null +++ b/tests/fixtures/registry/modules/applications/beta/home.nix @@ -0,0 +1,3 @@ +{ + test.homeValues = [ "beta-home" ]; +} diff --git a/tests/fixtures/registry/modules/namespace/helper.nix b/tests/fixtures/registry/modules/namespace/helper.nix new file mode 100644 index 0000000..c0edb1a --- /dev/null +++ b/tests/fixtures/registry/modules/namespace/helper.nix @@ -0,0 +1,3 @@ +{ + thisFileMustNotBeDiscovered = true; +} diff --git a/tests/fixtures/registry/modules/profiles/interface/test/meta.nix b/tests/fixtures/registry/modules/profiles/interface/test/meta.nix new file mode 100644 index 0000000..6f2061d --- /dev/null +++ b/tests/fixtures/registry/modules/profiles/interface/test/meta.nix @@ -0,0 +1,6 @@ +{ + includes = [ + "applications.alpha" + "applications.beta" + ]; +} diff --git a/tests/fixtures/registry/modules/services/nested/meta.nix b/tests/fixtures/registry/modules/services/nested/meta.nix new file mode 100644 index 0000000..c851c93 --- /dev/null +++ b/tests/fixtures/registry/modules/services/nested/meta.nix @@ -0,0 +1,11 @@ +{ lib, ... }: +{ + imports.nixos = [ + { + options.test.nested = lib.mkOption { + type = lib.types.bool; + default = false; + }; + } + ]; +} diff --git a/tests/fixtures/registry/modules/services/nested/nixos.nix b/tests/fixtures/registry/modules/services/nested/nixos.nix new file mode 100644 index 0000000..9cf542c --- /dev/null +++ b/tests/fixtures/registry/modules/services/nested/nixos.nix @@ -0,0 +1,3 @@ +{ + test.nested = true; +} diff --git a/tests/fixtures/registry/modules/users/test/home.nix b/tests/fixtures/registry/modules/users/test/home.nix new file mode 100644 index 0000000..4ce2994 --- /dev/null +++ b/tests/fixtures/registry/modules/users/test/home.nix @@ -0,0 +1,8 @@ +{ pkgs, ... }: +{ + home = { + username = "test"; + homeDirectory = if pkgs.stdenv.hostPlatform.isDarwin then "/Users/test" else "/home/test"; + stateVersion = "26.05"; + }; +} diff --git a/tests/registry.nix b/tests/registry.nix new file mode 100644 index 0000000..2cc21fb --- /dev/null +++ b/tests/registry.nix @@ -0,0 +1,134 @@ +{ + inputs, + lib, + pkgs, +}: +let + registry = import ../libs/registry.nix { + inherit inputs lib; + modulesRoot = ./fixtures/registry/modules; + }; + + hostLib = import ../libs/hosts.nix { + inherit inputs lib registry; + }; + + baseModule = { + options = { + home = { + username = lib.mkOption { type = lib.types.str; }; + homeDirectory = lib.mkOption { type = lib.types.str; }; + stateVersion = lib.mkOption { type = lib.types.str; }; + }; + }; + }; + + eval = + class: selected: + lib.evalModules { + specialArgs = { + inherit pkgs; + marker = "special-arg"; + }; + modules = [ + baseModule + (registry.mkModule { inherit class; }) + (registry.mkSelectionModule selected) + ]; + }; + + nixos = eval "nixos" [ "profiles.interface.test" ]; + darwin = eval "darwin" [ "applications.alpha" ]; + home = eval "home" [ "profiles.interface.test" ]; + + nixosHost = hostLib.mkNixos "registry-test" { + system = "x86_64-linux"; + user = "test"; + path = ./fixtures/registry/hosts; + profiles = [ "interface.test" ]; + }; + + darwinHost = hostLib.mkDarwin "registry-test-darwin" { + system = "aarch64-darwin"; + user = "test"; + path = ./fixtures/registry/hosts; + applications = [ "alpha" ]; + }; + + missingUnit = builtins.tryEval ( + builtins.deepSeq (registry.validateUnitIds [ "applications.missing" ]) true + ); + + invalidDependencyRegistry = import ../libs/registry.nix { + inherit inputs lib; + modulesRoot = ./fixtures/registry-invalid-dependency/modules; + }; + missingDependency = builtins.tryEval (builtins.deepSeq invalidDependencyRegistry.unitIds true); + + invalidFragmentRegistry = import ../libs/registry.nix { + inherit inputs lib; + modulesRoot = ./fixtures/registry-invalid-fragment/modules; + }; + invalidFragmentEvaluation = lib.evalModules { + modules = [ + (invalidFragmentRegistry.mkModule { class = "home"; }) + (invalidFragmentRegistry.mkSelectionModule [ "applications.broken" ]) + ]; + }; + invalidFragment = builtins.tryEval (builtins.deepSeq invalidFragmentEvaluation.config true); + + assertions = + assert + registry.unitIds == [ + "applications.alpha" + "applications.beta" + "profiles.interface.test" + "services.nested" + "users.test" + ]; + assert !(builtins.elem "namespace" registry.unitIds); + assert nixos.config.my.profiles.interface.test.enable; + assert nixos.config.my.applications.alpha.enable; + assert nixos.config.my.applications.beta.enable; + assert nixos.config.my.services.nested.enable; + assert nixos.config.test.nested; + assert + nixos.config.test.systemValues == [ + "common:special-arg" + "nixos" + ]; + assert nixos.config.test.external == "set-by-nixos-fragment"; + assert + darwin.config.test.systemValues == [ + "common:special-arg" + "darwin" + ]; + assert + home.config.test.homeValues == [ + "home" + "beta-home" + ]; + assert nixosHost.config.my.profiles.interface.test.enable; + assert nixosHost.config.home-manager.users.test.my.applications.alpha.enable; + assert + nixosHost.config.home-manager.users.test.test.homeValues == [ + "home" + "beta-home" + ]; + assert darwinHost.config.my.applications.alpha.enable; + assert darwinHost.config.home-manager.users.test.my.applications.alpha.enable; + assert darwinHost.config.home-manager.users.test.test.homeValues == [ "home" ]; + assert + darwinHost.config.test.systemValues == [ + "common:host" + "darwin" + ]; + assert !missingUnit.success; + assert !missingDependency.success; + assert !invalidFragment.success; + true; +in +assert assertions; +pkgs.runCommand "unit-registry-evaluation-tests" { } '' + touch "$out" +''