lol

lib/modules: Add class concept to check imports

This improves the error message when an incompatible module is
imported.

+69 -4
+22 -4
lib/modules.nix
··· 105 105 # when resolving module structure (like in imports). For everything else, 106 106 # there's _module.args. If specialArgs.modulesPath is defined it will be 107 107 # used as the base path for disabledModules. 108 + # 109 + # `specialArgs.class`: 110 + # A nominal type for modules. When set and non-null, this adds a check to 111 + # make sure that only compatible modules are imported. 108 112 specialArgs ? {} 109 113 , # This would be remove in the future, Prefer _module.args option instead. 110 114 args ? {} ··· 256 260 257 261 merged = 258 262 let collected = collectModules 263 + (specialArgs.class or null) 259 264 (specialArgs.modulesPath or "") 260 265 (regularModules ++ [ internalModule ]) 261 266 ({ inherit lib options config specialArgs; } // specialArgs); ··· 349 354 }; 350 355 in result; 351 356 352 - # collectModules :: (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ] 357 + # collectModules :: (class: String) -> (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ] 353 358 # 354 359 # Collects all modules recursively through `import` statements, filtering out 355 360 # all modules in disabledModules. 356 - collectModules = let 361 + collectModules = class: let 357 362 358 363 # Like unifyModuleSyntax, but also imports paths and calls functions if necessary 359 364 loadModule = args: fallbackFile: fallbackKey: m: ··· 364 369 throw "Module imports can't be nested lists. Perhaps you meant to remove one level of lists? Definitions: ${showDefs defs}" 365 370 else unifyModuleSyntax (toString m) (toString m) (applyModuleArgsIfFunction (toString m) (import m) args); 366 371 372 + checkModule = 373 + if class != null 374 + then 375 + m: 376 + if m.class != null -> m.class == class 377 + then m 378 + else 379 + throw "The module ${m._file or m.key} was imported into ${class} instead of ${m.class}." 380 + else 381 + m: m; 382 + 367 383 /* 368 384 Collects all modules recursively into the form 369 385 ··· 397 413 }; 398 414 in parentFile: parentKey: initialModules: args: collectResults (imap1 (n: x: 399 415 let 400 - module = loadModule args parentFile "${parentKey}:anon-${toString n}" x; 416 + module = checkModule (loadModule args parentFile "${parentKey}:anon-${toString n}" x); 401 417 collectedImports = collectStructuredModules module._file module.key module.imports args; 402 418 in { 403 419 key = module.key; ··· 461 477 else config; 462 478 in 463 479 if m ? config || m ? options then 464 - let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType"]; in 480 + let badAttrs = removeAttrs m ["_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType" "class"]; in 465 481 if badAttrs != {} then 466 482 throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by introducing a top-level `config' or `options' attribute. Add configuration attributes immediately on the top level instead, or move all of them (namely: ${toString (attrNames badAttrs)}) into the explicit `config' attribute." 467 483 else ··· 471 487 imports = m.imports or []; 472 488 options = m.options or {}; 473 489 config = addFreeformType (addMeta (m.config or {})); 490 + class = m.class or null; 474 491 } 475 492 else 476 493 # shorthand syntax ··· 481 498 imports = m.require or [] ++ m.imports or []; 482 499 options = {}; 483 500 config = addFreeformType (removeAttrs m ["_file" "key" "disabledModules" "require" "imports" "freeformType"]); 501 + class = m.class or null; 484 502 }; 485 503 486 504 applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: if isFunction f then
+5
lib/tests/modules.sh
··· 360 360 # because of an `extendModules` bug, issue 168767. 361 361 checkConfigOutput '^1$' config.sub.specialisation.value ./extendModules-168767-imports.nix 362 362 363 + # Class checks 364 + checkConfigOutput '^{ }$' config.ok.config ./class-check.nix 365 + checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix 366 + checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix 367 + 363 368 # doRename works when `warnings` does not exist. 364 369 checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix 365 370 # doRename adds a warning.
+34
lib/tests/modules/class-check.nix
··· 1 + { lib, ... }: { 2 + config = { 3 + _module.freeformType = lib.types.anything; 4 + ok = 5 + lib.evalModules { 6 + specialArgs.class = "nixos"; 7 + modules = [ 8 + ./module-class-is-nixos.nix 9 + ]; 10 + }; 11 + 12 + fail = 13 + lib.evalModules { 14 + specialArgs.class = "nixos"; 15 + modules = [ 16 + ./module-class-is-nixos.nix 17 + ./module-class-is-darwin.nix 18 + ]; 19 + }; 20 + 21 + fail-anon = 22 + lib.evalModules { 23 + specialArgs.class = "nixos"; 24 + modules = [ 25 + ./module-class-is-nixos.nix 26 + { _file = "foo.nix#darwinModules.default"; 27 + class = "darwin"; 28 + imports = []; 29 + } 30 + ]; 31 + }; 32 + 33 + }; 34 + }
+4
lib/tests/modules/module-class-is-darwin.nix
··· 1 + { 2 + class = "darwin"; 3 + config = {}; 4 + }
+4
lib/tests/modules/module-class-is-nixos.nix
··· 1 + { 2 + class = "nixos"; 3 + config = {}; 4 + }