From 5186921dedc57d054db8ae6ebfd9fa3385197c45 Mon Sep 17 00:00:00 2001 From: "Shahar \"Dawn\" Or" Date: Fri, 4 Jul 2025 23:40:53 -0600 Subject: [PATCH] lib.evalModules: add graph attribute Co-authored-by: Ali Jamadi --- doc/module-system/module-system.chapter.md | 10 ++++ doc/redirects.json | 3 + lib/modules.nix | 62 +++++++++++++-------- lib/tests/modules.sh | 23 +++++++- lib/tests/modules/graph/a.nix | 8 +++ lib/tests/modules/graph/b.nix | 3 + lib/tests/modules/graph/test.nix | 64 ++++++++++++++++++++++ 7 files changed, 150 insertions(+), 23 deletions(-) create mode 100644 lib/tests/modules/graph/a.nix create mode 100644 lib/tests/modules/graph/b.nix create mode 100644 lib/tests/modules/graph/test.nix diff --git a/doc/module-system/module-system.chapter.md b/doc/module-system/module-system.chapter.md index 8c27aea15ffc..8e95580522b2 100644 --- a/doc/module-system/module-system.chapter.md +++ b/doc/module-system/module-system.chapter.md @@ -116,6 +116,16 @@ A nominal type marker, always `"configuration"`. The [`class` argument](#module-system-lib-evalModules-param-class). +#### `graph` {#module-system-lib-evalModules-return-value-graph} + +Represents all the modules that took part in the evaluation. +It is a list of `ModuleGraph` where `ModuleGraph` is defined as an attribute set with the following attributes: + +- `key`: `string` for the purpose of module deduplication and `disabledModules` +- `file`: `string` for the purpose of error messages and warnings +- `imports`: `[ ModuleGraph ]` +- `disabled`: `bool` + ## Module arguments {#module-system-module-arguments} Module arguments are the attribute values passed to modules when they are evaluated. diff --git a/doc/redirects.json b/doc/redirects.json index 8f7810698bbc..8210470b5bcb 100644 --- a/doc/redirects.json +++ b/doc/redirects.json @@ -487,6 +487,9 @@ "module-system-lib-evalModules-return-value-_configurationClass": [ "index.html#module-system-lib-evalModules-return-value-_configurationClass" ], + "module-system-lib-evalModules-return-value-graph": [ + "index.html#module-system-lib-evalModules-return-value-graph" + ], "part-stdenv": [ "index.html#part-stdenv" ], diff --git a/lib/modules.nix b/lib/modules.nix index 76e6de892e82..cc3148b0eea7 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -245,25 +245,26 @@ let }; }; - merged = - let - collected = - collectModules class (specialArgs.modulesPath or "") (regularModules ++ [ internalModule ]) - ( - { - inherit - lib - options - specialArgs - ; - _class = class; - _prefix = prefix; - config = addErrorContext "if you get an infinite recursion here, you probably reference `config` in `imports`. If you are trying to achieve a conditional import behavior dependent on `config`, consider importing unconditionally, and using `mkEnableOption` and `mkIf` to control its effect." config; - } - // specialArgs - ); - in - mergeModules prefix (reverseList collected); + # This function takes an empty attrset as an argument. + # It could theoretically be replaced with its body, + # but such a binding is avoided to allow for earlier grabage collection. + doCollect = + { }: + collectModules class (specialArgs.modulesPath or "") (regularModules ++ [ internalModule ]) ( + { + inherit + lib + options + specialArgs + ; + _class = class; + _prefix = prefix; + config = addErrorContext "if you get an infinite recursion here, you probably reference `config` in `imports`. If you are trying to achieve a conditional import behavior dependent on `config`, consider importing unconditionally, and using `mkEnableOption` and `mkIf` to control its effect." config; + } + // specialArgs + ); + + merged = mergeModules prefix (reverseList (doCollect { }).modules); options = merged.matchedOptions; @@ -359,12 +360,13 @@ let options = checked options; config = checked (removeAttrs config [ "_module" ]); _module = checked (config._module); + inherit (doCollect { }) graph; inherit extendModules type class; }; in result; - # collectModules :: (class: String) -> (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ] + # collectModules :: (class: String) -> (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> ModulesTree # # Collects all modules recursively through `import` statements, filtering out # all modules in disabledModules. @@ -529,9 +531,25 @@ let operator = attrs: keyFilter attrs.modules; }); + toGraph = + modulesPath: + { disabled, modules }: + let + isDisabledModule = isDisabled modulesPath disabled; + + toModuleGraph = structuredModule: { + disabled = isDisabledModule structuredModule; + inherit (structuredModule) key; + file = structuredModule.module._file; + imports = map toModuleGraph structuredModule.modules; + }; + in + map toModuleGraph (filter (x: x.key != "lib/modules.nix") modules); in - modulesPath: initialModules: args: - filterModules modulesPath (collectStructuredModules unknownModule "" initialModules args); + modulesPath: initialModules: args: { + modules = filterModules modulesPath (collectStructuredModules unknownModule "" initialModules args); + graph = toGraph modulesPath (collectStructuredModules unknownModule "" initialModules args); + }; /** Wrap a module with a default location for reporting errors. diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index a4aa5201715c..301808ae6651 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -20,6 +20,10 @@ cd "$DIR"/modules pass=0 fail=0 +local-nix-instantiate() { + nix-instantiate --timeout 1 --eval-only --show-trace --read-write-mode --json "$@" +} + # loc # prints the location of the call of to the function that calls it # loc n @@ -55,7 +59,7 @@ evalConfig() { local attr=$1 shift local script="import ./default.nix { modules = [ $* ];}" - nix-instantiate --timeout 1 -E "$script" -A "$attr" --eval-only --show-trace --read-write-mode --json + local-nix-instantiate -E "$script" -A "$attr" } reportFailure() { @@ -106,6 +110,20 @@ globalErrorLogCheck() { } } +checkExpression() { + local path=$1 + local output + { + output="$(local-nix-instantiate --strict "$path" 2>&1)" && ((++pass)) + } || { + logStartFailure + echo "$output" + ((++fail)) + logFailure + logEndFailure + } +} + checkConfigError() { local errorContains=$1 local err="" @@ -337,6 +355,9 @@ checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix checkConfigError 'A definition for option .* is not of type .*. Definition values:\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix +# Check `graph` attribute +checkExpression './graph/test.nix' + # Check mkAliasOptionModule. checkConfigOutput '^true$' config.enable ./alias-with-priority.nix checkConfigOutput '^true$' config.enableAlias ./alias-with-priority.nix diff --git a/lib/tests/modules/graph/a.nix b/lib/tests/modules/graph/a.nix new file mode 100644 index 000000000000..e3c72afbe672 --- /dev/null +++ b/lib/tests/modules/graph/a.nix @@ -0,0 +1,8 @@ +{ + imports = [ + { + imports = [ { } ]; + } + ]; + disabledModules = [ ./b.nix ]; +} diff --git a/lib/tests/modules/graph/b.nix b/lib/tests/modules/graph/b.nix new file mode 100644 index 000000000000..a890d3138a18 --- /dev/null +++ b/lib/tests/modules/graph/b.nix @@ -0,0 +1,3 @@ +args: { + imports = [ { key = "explicit-key"; } ]; +} diff --git a/lib/tests/modules/graph/test.nix b/lib/tests/modules/graph/test.nix new file mode 100644 index 000000000000..748fb8db5f4d --- /dev/null +++ b/lib/tests/modules/graph/test.nix @@ -0,0 +1,64 @@ +let + lib = import ../../..; + + evaluation = lib.evalModules { + modules = [ + { } + (args: { }) + ./a.nix + ./b.nix + ]; + }; + + actual = evaluation.graph; + + expected = [ + { + key = ":anon-1"; + file = ""; + imports = [ ]; + disabled = false; + } + { + key = ":anon-2"; + file = ""; + imports = [ ]; + disabled = false; + } + { + key = toString ./a.nix; + file = toString ./a.nix; + imports = [ + { + key = "${toString ./a.nix}:anon-1"; + file = toString ./a.nix; + imports = [ + { + key = "${toString ./a.nix}:anon-1:anon-1"; + file = toString ./a.nix; + imports = [ ]; + disabled = false; + } + ]; + disabled = false; + } + ]; + disabled = false; + } + { + key = toString ./b.nix; + file = toString ./b.nix; + imports = [ + { + key = "explicit-key"; + file = toString ./b.nix; + imports = [ ]; + disabled = false; + } + ]; + disabled = true; + } + ]; +in +assert actual == expected; +null