modularizer: init.

+1
.gitignore
··· 1 + flake.lock
+22
flake.nix
··· 1 + { 2 + description = "Modules too niche or small to be worth sharing as their own flake."; 3 + 4 + inputs = { 5 + flake-parts.url = "github:hercules-ci/flake-parts"; 6 + }; 7 + 8 + outputs = inputs@ { flake-parts, ... }: flake-parts.lib.mkFlake { 9 + inherit inputs; 10 + } (inputs@ { ... }: let 11 + 12 + modularizer = import ./modularizer.nix; 13 + 14 + in { 15 + 16 + flake.flakeModules.default = modularizer; 17 + flake.flakeModules.modularizer = modularizer; 18 + 19 + systems = [ "x86_64-linux" ]; 20 + debug = false; 21 + }); 22 + }
+151
modularizer.nix
··· 1 + { self, lib, config, ... }: let 2 + cfg = config.modularizer; 3 + in { 4 + options.modularizer = { 5 + paths = lib.mkOption { 6 + type = lib.types.listOf lib.types.path; 7 + default = []; 8 + description = "List of paths to import"; 9 + }; 10 + 11 + enableDefaultModules = lib.mkOption { 12 + type = lib.types.bool; 13 + default = true; 14 + description = "Enable default modules"; 15 + }; 16 + 17 + modules = lib.mkOption { 18 + # ./path/name/type.nix or ./path/name/type/default.nix 19 + # --> becomes --> 20 + # { "type" = { "name" = f: import ./path/name/type.nix; }; } or 21 + # { "type" = { "name" = f: import ./path/name/type/default.nix; }; } 22 + # ./path/ can be longer, like ./path/path/path/name/type.nix 23 + # the recursive search will stop in any directory with a default.nix, 24 + # allowing for either single .nix files for each type or an entire directory. 25 + type = with lib.types; attrsOf (attrsOf (functionTo attrs)); 26 + 27 + internal = true; 28 + readOnly = true; 29 + 30 + default = let 31 + 32 + inherit (builtins) 33 + elemAt 34 + length 35 + isFunction 36 + groupBy 37 + mapAttrs 38 + listToAttrs 39 + readFileType 40 + ; 41 + 42 + inherit (lib) 43 + concatMapAttrs 44 + hasSuffix 45 + removeSuffix 46 + pathExists 47 + splitString 48 + attrsToList 49 + concatMap 50 + ; 51 + 52 + importFromDirectoryRecursive = 53 + directory: 54 + let 55 + processDir = 56 + { 57 + basePath, 58 + localPath ? "", # should be emtpy or start with "/" 59 + }: 60 + let 61 + fullPathDir = basePath + localPath; 62 + in 63 + concatMapAttrs ( 64 + name: type: 65 + let 66 + fullPath = fullPathDir + "/" + name; 67 + isFile = type == "regular"; 68 + isDir = type == "directory"; 69 + isImportableDir = 70 + isDir && 71 + pathExists (fullPath + "/" + "default.nix") && 72 + readFileType (fullPath + "/" + "default.nix") == "regular"; 73 + isSupported = 74 + isFile || # regular file 75 + (isDir && !isImportableDir) || # dir with no default.nix file 76 + isImportableDir # dir where default.nix is a regular file 77 + ; 78 + in 79 + 80 + # unknown type 81 + if !isSupported then 82 + throw '' 83 + modulesFromDirectoryRecursive: Unsupported file type ${type} at path ${toString fullPath} 84 + '' 85 + 86 + # duplicate importable files with the same name, 87 + # directory/default.nix and directory.nix both exist 88 + else if 89 + isImportableDir && 90 + pathExists (fullPath + ".nix") 91 + then 92 + throw '' 93 + modulesFromDirectoryRecursive: Found both a directory ${fullPath} and file ${fullPath}/default.nix which conflict (same module name). 94 + Suggested action: Delete the ${name}.nix file and move its contents into ${name}/default.nix or vice versa. 95 + '' 96 + 97 + # .nix file or directory with default.nix 98 + else if 99 + (isFile && hasSuffix ".nix" name) || 100 + isImportableDir 101 + then 102 + { 103 + # FIXME: importing an empty file or otherwise a non-nix file will freeze execution... 104 + "${localPath + "/" + (removeSuffix ".nix" name)}" = import fullPath; 105 + } 106 + 107 + # directory without default.nix 108 + else if isDir && !isImportableDir then 109 + processDir 110 + { 111 + inherit basePath; 112 + localPath = localPath + "/" + name; 113 + } 114 + 115 + # non-nix file 116 + else 117 + { } 118 + ) (builtins.readDir fullPathDir); 119 + in processDir { basePath = directory; }; 120 + 121 + processImportedModule = m: 122 + let 123 + pathParts = splitString "/" m.name; 124 + type = elemAt pathParts (length pathParts - 1); 125 + # first element is an empty string because of the root slash 126 + # [ "" "moduleName" ... "type" ] 127 + name = if length pathParts >= 3 then elemAt pathParts 1 else "default"; 128 + value = if isFunction m.value then m.value else _: m.value; 129 + in { 130 + inherit type name value; 131 + }; 132 + 133 + allDirs = map importFromDirectoryRecursive cfg.paths; 134 + allImports = concatMap attrsToList allDirs; 135 + allModules = map processImportedModule allImports; 136 + 137 + byType = groupBy (e: e.type) allModules; 138 + finalize = mapAttrs (_: v: listToAttrs v) byType; 139 + 140 + in finalize; 141 + }; 142 + 143 + }; 144 + 145 + config.modularizer = { 146 + paths = 147 + let 148 + path = self + "/modules"; 149 + in lib.mkIf (cfg.enableDefaultModules && builtins.pathExists path) [ path ]; 150 + }; 151 + }