Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions configs/flake/checks/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<name>-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;
Expand Down
142 changes: 110 additions & 32 deletions lib-overlays/integrations/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2376,8 +2376,9 @@
# modules it imports. `options` are the options of the evaluation.
# A registry entry is a module whose key is
# `modules.<class>.<name>@<project>` (`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`,
Expand All @@ -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
Expand All @@ -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.
Expand Down
51 changes: 51 additions & 0 deletions tests/pinned-world/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
{ ... }:
{ }
Original file line number Diff line number Diff line change
@@ -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";
}
20 changes: 20 additions & 0 deletions tests/pinned-world/registries/modules/nixos/importer/default.nix
Original file line number Diff line number Diff line change
@@ -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";
}
11 changes: 11 additions & 0 deletions tests/pinned-world/registries/modules/nixos/importer/option.nix
Original file line number Diff line number Diff line change
@@ -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.";
};
}
Loading