nix registory

This commit is contained in:
2026-07-27 10:58:24 +09:00
parent 1d82ab92b6
commit 5b1bde7d90
24 changed files with 825 additions and 0 deletions
+4
View File
@@ -24,4 +24,8 @@
!/hosts/ !/hosts/
!/libs/
!/modules/ !/modules/
!/tests/
Generated
+22
View File
@@ -570,6 +570,27 @@
"type": "github" "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": { "nix-index-database": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -962,6 +983,7 @@
"lanzaboote": "lanzaboote", "lanzaboote": "lanzaboote",
"llm-agents": "llm-agents", "llm-agents": "llm-agents",
"niri-flake": "niri-flake", "niri-flake": "niri-flake",
"nix-darwin": "nix-darwin",
"nix-index-database": "nix-index-database", "nix-index-database": "nix-index-database",
"nixos-hardware": "nixos-hardware", "nixos-hardware": "nixos-hardware",
"nixos-wsl": "nixos-wsl", "nixos-wsl": "nixos-wsl",
+5
View File
@@ -11,6 +11,11 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
nix-darwin = {
url = "github:nix-darwin/nix-darwin/nix-darwin-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
# Hardware / Platform # Hardware / Platform
nixos-hardware.url = "github:NixOS/nixos-hardware/master"; nixos-hardware.url = "github:NixOS/nixos-hardware/master";
nixos-wsl.url = "github:nix-community/NixOS-WSL"; nixos-wsl.url = "github:nix-community/NixOS-WSL";
+1
View File
@@ -2,5 +2,6 @@
imports = [ imports = [
./formatter.nix ./formatter.nix
./git-hooks.nix ./git-hooks.nix
./registry.nix
]; ];
} }
+42
View File
@@ -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;
};
};
}
+17
View File
@@ -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;
}
+168
View File
@@ -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
;
}
+324
View File
@@ -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.<class> 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
;
}
@@ -0,0 +1,3 @@
{
includes = [ "applications.missing" ];
}
@@ -0,0 +1,3 @@
{
imports = [ ];
}
+3
View File
@@ -0,0 +1,3 @@
{
system.stateVersion = 6;
}
+9
View File
@@ -0,0 +1,9 @@
{
fileSystems."/" = {
device = "/dev/null";
fsType = "ext4";
};
boot.loader.grub.enable = false;
system.stateVersion = "26.05";
}
@@ -0,0 +1,7 @@
{
marker ? "host",
...
}:
{
test.systemValues = [ "common:${marker}" ];
}
@@ -0,0 +1,3 @@
{
test.systemValues = [ "darwin" ];
}
@@ -0,0 +1,3 @@
{
test.homeValues = [ "home" ];
}
@@ -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 = [ ];
};
}
];
}
@@ -0,0 +1,4 @@
{
test.systemValues = [ "nixos" ];
test.external = "set-by-nixos-fragment";
}
@@ -0,0 +1,3 @@
{
test.homeValues = [ "beta-home" ];
}
+3
View File
@@ -0,0 +1,3 @@
{
thisFileMustNotBeDiscovered = true;
}
@@ -0,0 +1,6 @@
{
includes = [
"applications.alpha"
"applications.beta"
];
}
@@ -0,0 +1,11 @@
{ lib, ... }:
{
imports.nixos = [
{
options.test.nested = lib.mkOption {
type = lib.types.bool;
default = false;
};
}
];
}
@@ -0,0 +1,3 @@
{
test.nested = true;
}
+8
View File
@@ -0,0 +1,8 @@
{ pkgs, ... }:
{
home = {
username = "test";
homeDirectory = if pkgs.stdenv.hostPlatform.isDarwin then "/Users/test" else "/home/test";
stateVersion = "26.05";
};
}
+134
View File
@@ -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"
''