diff --git a/configs/flake/checks/default.nix b/configs/flake/checks/default.nix index de8ba58..c19d893 100644 --- a/configs/flake/checks/default.nix +++ b/configs/flake/checks/default.nix @@ -190,26 +190,33 @@ in } ''; - # The checks of a test flake for this system, without its check - # `registries`. A test flake that is a caisson top has the check - # `registries`, and the caisson top of caisson publishes the check - # `registries` of caisson under that name. - testChecksOf = outputs: builtins.removeAttrs outputs.checks.${system} [ "registries" ]; + # The checks of the test flake `name` for this system. A test + # flake that is a caisson top has the check `registries`, and the + # caisson top of caisson publishes the check `registries` of + # caisson under that name, so the check `registries` of the test + # flake is published as `-registries`. + testChecksOf = + name: outputs: + let + checks = outputs.checks.${system}; + in + builtins.removeAttrs checks [ "registries" ] + // (if checks ? registries then { "${name}-registries" = checks.registries; } else { }); # The checks for x86_64-linux. linuxChecks = - testChecksOf integrationOutputs - // testChecksOf minimalConsumerOutputs - // testChecksOf moduleClassExportOutputs - // testChecksOf libConsumerChainFinalOutputs - // testChecksOf nixpkgsConsumerOutputs - // testChecksOf nixpkgsOverlayExportOutputs - // testChecksOf nixpkgsNoPkgSetsOutputs - // testChecksOf nixpkgsPkgSetsOutputs - // testChecksOf nixpkgsLibOnlyOutputs - // testChecksOf pkgOverlaysProducerOutputs - // testChecksOf pkgOverlaysConsumerOutputs - // testChecksOf unitTestOutputs + testChecksOf "basic-composition" integrationOutputs + // testChecksOf "minimal-consumer" minimalConsumerOutputs + // testChecksOf "module-class-export" moduleClassExportOutputs + // testChecksOf "lib-consumer-chain" libConsumerChainFinalOutputs + // testChecksOf "nixpkgs-consumer" nixpkgsConsumerOutputs + // testChecksOf "nixpkgs-overlay-export" nixpkgsOverlayExportOutputs + // testChecksOf "nixpkgs-no-pkg-sets" nixpkgsNoPkgSetsOutputs + // testChecksOf "nixpkgs-pkg-sets" nixpkgsPkgSetsOutputs + // testChecksOf "nixpkgs-lib-only" nixpkgsLibOnlyOutputs + // testChecksOf "pkg-overlays-registry-producer" pkgOverlaysProducerOutputs + // testChecksOf "pkg-overlays-registry-consumer" pkgOverlaysConsumerOutputs + // testChecksOf "unit" unitTestOutputs // { literate-flake-default = exampleOutputs.packages.${system}.default; literate-flake-greeting = exampleOutputs.packages.${system}.greeting; diff --git a/lib-overlays/integrations/default.nix b/lib-overlays/integrations/default.nix index 55aa934..abeb8e0 100644 --- a/lib-overlays/integrations/default.nix +++ b/lib-overlays/integrations/default.nix @@ -2376,8 +2376,9 @@ # modules it imports. `options` are the options of the evaluation. # A registry entry is a module whose key is # `modules..@` (`keyedModules`). The files - # of an entry are the files of the entry and of every module it - # imports, at any depth. The result holds each entry by registry + # of an entry are the files that the modules the entry imports, + # at any depth, name, where a module stands for every module of + # the graph with its key. The result holds each entry by registry # key, with `project`, `file`, and `defines`: for each option # under `caisson` that a file of the entry defines, the option # as `option`, the files of the entry that define it as `files`, @@ -2399,41 +2400,118 @@ [ ]; caissonOptions = optionsUnder (options.caisson or { }); enabled = builtins.filter (node: !(node.disabled or false)); - filesOf = node: [ node.file ] ++ builtins.concatMap filesOf (enabled node.imports); - entriesIn = - nodes: + # The modules that a module imports, at any depth, that name + # their file, each as `file` and `wraps`. The module system + # gives a module that names no file the file of the module + # that imports it. A module therefore names its file when its + # file differs from the file of the module that imports it. + # `wraps` is true when the module imports a single module and + # that module names its file, as the module that + # `lib.types.deferredModule` makes of each definition does. + namedBelow = + node: + builtins.concatMap ( + imported: + let + importedByIt = enabled imported.imports; + in + ( + if imported.file != node.file then + [ + { + inherit (imported) file; + wraps = builtins.length importedByIt == 1 && (builtins.head importedByIt).file != imported.file; + } + ] + else + [ ] + ) + ++ namedBelow imported + ) (enabled node.imports); + # Every module of the graph, depth first, as `node`, with the + # file of the module that imports it as `importer`, null for + # a module that `lib.evalModules` was given. + occurrencesIn = + importer: nodes: builtins.concatMap ( - node: - (if builtins.substring 0 8 (toString (node.key or "")) == "modules." then [ node ] else [ ]) - ++ entriesIn (enabled node.imports) + node: [ { inherit node importer; } ] ++ occurrencesIn node.file (enabled node.imports) ) (enabled nodes); + occurrences = occurrencesIn null graph; + # The key of a module as an attribute name. The key of a + # module read from a file is the path of the file, a string + # that refers to a store path, and an attribute name may not + # refer to one. + keyOf = node: builtins.unsafeDiscardStringContext (toString node.key); + # The modules of the graph by key, each with `files`, the + # files that the modules of the key name, and `imports`, the + # keys of the modules they import, over every place the graph + # holds a module of the key. + # + # The module system evaluates the modules that share a key + # once, at the first place it reaches one of them, and the + # graph holds each of them where it is imported. The copies + # of a project in different store paths give a module the + # same key and different files: a flake that pins a revision + # of caisson and lists a project that pins another revision + # imports the core module of caisson from each copy. The + # declarations of the options of the + # core module are then in the files of the copy the module + # system reached first. A module that imports the other copy + # imports those declarations through the key they share. + byKey = builtins.mapAttrs (_: found: { + files = unique ( + builtins.concatMap ( + occurrence: if occurrence.node.file != occurrence.importer then [ occurrence.node.file ] else [ ] + ) found + ); + imports = unique ( + builtins.concatMap (occurrence: builtins.map keyOf (enabled occurrence.node.imports)) found + ); + }) (builtins.groupBy (occurrence: keyOf occurrence.node) occurrences); + entries = builtins.filter (node: builtins.substring 0 8 (toString (node.key or "")) == "modules.") ( + builtins.map (occurrence: occurrence.node) occurrences + ); entryOf = node: let - # The registry wraps the module of an entry in modules that - # each import the next module alone. A wrapper names no - # file, so the module system gives it the file of the - # module that imports the entry, such as the file of the - # configuration that selects it. The module of the entry is - # the first module below the wrappers whose file differs - # from the file of the wrappers, and the files of the entry - # are the files of that module and of every module it - # imports. - below = - wrapper: - let - imported = enabled wrapper.imports; - in - if builtins.length imported != 1 then - wrapper - else if (builtins.head imported).file == node.file then - below (builtins.head imported) + # The registry wraps the module of an entry in modules + # that name no file, and the module system gives each + # wrapper the file of the module that imports the entry. + # When a configuration module imports the entry, the + # wrappers carry the file of that configuration module, + # and the definitions that the configuration module makes + # carry the same file. The files of the entry are therefore + # the files that the modules below the entry name, and a + # definition belongs to the entry only when one of these + # files makes it. The modules below the entry are those of + # the keys the entry reaches through the imports of each + # key (`byKey`). The file shown for the entry is the first + # file named below the entry (`namedBelow`) whose module + # does more than wrap a single module that names its file. + reached = builtins.genericClosure { + startSet = [ { key = keyOf node; } ]; + operator = found: builtins.map (key: { inherit key; }) byKey.${found.key}.imports; + }; + named = namedBelow node; + shown = builtins.filter (module: !module.wraps) named; + file = + if shown != [ ] then + (builtins.head shown).file + else if named != [ ] then + (builtins.head named).file else - builtins.head imported; - module = below node; - inherit (module) file; - files = unique (filesOf module); - inEntry = file: builtins.elem file files; + node.file; + # The files of the entry as attribute names. + files = builtins.listToAttrs ( + builtins.concatMap ( + found: + builtins.map (file: { + name = builtins.unsafeDiscardStringContext (toString file); + value = true; + }) byKey.${found.key}.files + ) reached + ); + inEntry = file: files ? ${builtins.unsafeDiscardStringContext (toString file)}; defined = builtins.filter (option: builtins.any inEntry option.files) caissonOptions; registered = registryPartsOf node.key; in @@ -2451,7 +2529,7 @@ }; }; in - builtins.listToAttrs (builtins.map entryOf (entriesIn graph)); + builtins.listToAttrs (builtins.map entryOf entries); # The check `registries` of a caisson top for `system`, by name, # the default check every caisson top carries. diff --git a/tests/pinned-world/default.nix b/tests/pinned-world/default.nix index 6896de9..aa05d59 100644 --- a/tests/pinned-world/default.nix +++ b/tests/pinned-world/default.nix @@ -7187,6 +7187,57 @@ let in top.checks.x86_64-linux.registries.message == ""; + # The registry entry `caisson/activation-marker` of the generic + # class, from the project caisson, imports the module that caisson exports for it through a + # module that names no file and imports several modules. The module + # system gives that module the file of the module that imports the + # entry. The module `importer` imports the entry, and defines + # `caisson.importerNote` in its file. The check `registries` takes + # the files of the entry from the modules below it that name their + # file. It records the definition for `importer`, which imports the + # declaration of the option, and not for + # `caisson/activation-marker`, which does not, and it passes. + registriesIgnoresTheDefinitionsOfTheModuleThatImportsAnEntry = + let + top = registriesTopWith "importer"; + # The registry entries of the NixOS configuration `nas-1`. + entries = builtins.foldl' (found: manifest: found // manifest.registryEntries or { }) { } ( + builtins.filter (manifest: manifest.type or null == "nixos") (manifestsOf top.caisson.manifest) + ); + in + top.checks.x86_64-linux.registries.message == "" + && builtins.all ( + definition: definition.option != "caisson.importerNote" + ) entries."modules.generic.caisson/activation-marker".defines + && entries."modules.generic.caisson/activation-marker".file == "modules/generic/activation-marker" + && builtins.any ( + definition: + definition.option == "caisson.importerNote" + && definition.files == [ "tests/pinned-world/registries/modules/nixos/importer" ] + && definition.declared + ) entries."modules.nixos.importer".defines; + + # The module system evaluates the modules that share a key once, + # so the declarations of the options of the core module of caisson + # are in the files of the copy of the core module it reaches + # first. The module `copied-core` imports a module at another file + # under the key of the core module, as a project that pins another + # revision of caisson does, and defines an option that the core + # module declares. The check `registries` follows the key to the + # files of the core module that caisson imports, and passes. + registriesFollowsTheKeyOfAModuleToItsDeclarations = + let + top = registriesTopWith "copied-core"; + # The registry entries of the NixOS configuration `nas-1`. + entries = builtins.foldl' (found: manifest: found // manifest.registryEntries or { }) { } ( + builtins.filter (manifest: manifest.type or null == "nixos") (manifestsOf top.caisson.manifest) + ); + in + top.checks.x86_64-linux.registries.message == "" + && builtins.any ( + definition: definition.option == "caisson.structural.exportNote" && definition.declared + ) entries."modules.nixos.copied-core".defines; + # The check `registries` fails to build where a registry entry # defines an option under `caisson` and imports no module that # declares it, as the module `undeclared` does. Its message names diff --git a/tests/pinned-world/registries/modules/nixos/copied-core/copy.nix b/tests/pinned-world/registries/modules/nixos/copied-core/copy.nix new file mode 100644 index 0000000..8d1b2de --- /dev/null +++ b/tests/pinned-world/registries/modules/nixos/copied-core/copy.nix @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: MIT +# +# The stand-in for the core module of caisson in another copy of +# caisson, which the module `copied-core` imports under the key of the +# core module. +{ ... }: +{ } diff --git a/tests/pinned-world/registries/modules/nixos/copied-core/default.nix b/tests/pinned-world/registries/modules/nixos/copied-core/default.nix new file mode 100644 index 0000000..fd62278 --- /dev/null +++ b/tests/pinned-world/registries/modules/nixos/copied-core/default.nix @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT +# +# A NixOS module that defines an option under `caisson` and imports a +# second copy of the core module of caisson for the nixos class. A +# project that pins another revision of caisson imports the core +# module from that revision: the module has the key that the registry +# gives the core module, and its files are in another store path. Here +# the module `copy.nix` beside this file stands for that copy, under +# the key of the module that the registry entry `caisson/core` imports. +# The module system evaluates the modules that share a key once, and +# caisson imports the core module into the NixOS configuration first, +# so `copy.nix` is not evaluated. The check +# `registriesFollowsTheKeyOfAModuleToItsDeclarations` of the +# pinned-world suite selects this module and reads that the check +# `registries` passes. +{ closure-lib, ... }: +{ ... }: +let + # The module that the registry entry `caisson/core` imports, keyed + # `registered.modules.nixos.core@caisson`. + registered = builtins.head closure-lib.caisson.modules.nixos."caisson/core".imports; +in +{ + imports = [ + { + inherit (registered) key; + imports = [ ./copy.nix ]; + } + ]; + caisson.structural.exportNote = "a note of the module that imports a copy of the core module"; +} diff --git a/tests/pinned-world/registries/modules/nixos/importer/default.nix b/tests/pinned-world/registries/modules/nixos/importer/default.nix new file mode 100644 index 0000000..f7bdc4a --- /dev/null +++ b/tests/pinned-world/registries/modules/nixos/importer/default.nix @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: MIT +# +# A NixOS module that imports the registry entry +# `caisson/activation-marker` of the generic class and defines the option +# `caisson.importerNote`, which the file `option.nix` beside this file +# declares. `caisson/activation-marker` does not import `option.nix`. +# The registry gives the wrappers of `caisson/activation-marker` the +# file of this module, and this module defines the option in the same +# file. The check `registriesIgnoresTheDefinitionsOfTheModuleThatImportsAnEntry` +# of the pinned-world suite selects this module and reads that the +# check `registries` passes. +{ closure-lib, ... }: +{ ... }: +{ + imports = [ + ./option.nix + closure-lib.caisson.modules.generic."caisson/activation-marker" + ]; + caisson.importerNote = "a note of the importer module"; +} diff --git a/tests/pinned-world/registries/modules/nixos/importer/option.nix b/tests/pinned-world/registries/modules/nixos/importer/option.nix new file mode 100644 index 0000000..fc2b178 --- /dev/null +++ b/tests/pinned-world/registries/modules/nixos/importer/option.nix @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: MIT +# +# The declaration of `caisson.importerNote`, which the module +# `importer` imports and defines. +{ lib, ... }: +{ + options.caisson.importerNote = lib.mkOption { + type = lib.types.str; + description = "A note that the module `importer` defines."; + }; +}