mirror of
https://github.com/nix-community/dream2nix.git
synced 2024-12-23 14:31:55 +03:00
108 lines
4.9 KiB
Markdown
108 lines
4.9 KiB
Markdown
|
# Thoughts on the current nixos module system
|
||
|
|
||
|
This document covers observed problems with the current nixos module system that arose after using it for dream2nix and proposes changes to the module system.
|
||
|
|
||
|
## Problem 1: Bad control over module dependencies
|
||
|
|
||
|
It is easy to depend on a module unintentionally.
|
||
|
|
||
|
It is hard to guarantee that a module works with a limited set of other modules.
|
||
|
|
||
|
A module can import arbitrary paths, which makes it hard to limit the modules of the evaluation.
|
||
|
|
||
|
## Problem 2: Module identification/duplication issues
|
||
|
|
||
|
References to the same module in different ways (file vs imported file), are sometimes accidentally detected as two different modules leading to a collision error.
|
||
|
|
||
|
It is possible to prevent this by setting `_file`, but this still not optimal, as not all modules are forced to define this field.
|
||
|
Not having a unique ID by default is not optimal.
|
||
|
|
||
|
## Problem 3: Result type and location not discoverable
|
||
|
|
||
|
Usually the evaluation of a set of modules leads to a result or a set of results. But neither the type of the result nor the location of the result within `config` can be discovered by the caller without looking at the modules implementation.
|
||
|
|
||
|
For example NixOS exports the results via `config.system`, drv-parts uses `config.public` etc.
|
||
|
For the user calling `evalModules` it is not clear how exactly to get to the final result out of the evaluation.
|
||
|
|
||
|
Strictly speaking, the result is the `config`, but apart from the final result, `config` also contains other things, like user interfaces and intermediary result. This confuses a user who is only interested in the final result.
|
||
|
|
||
|
## Problem 4: Unprotected gloabl scope
|
||
|
Using the global scope to pass data between modules is not optimal, because:
|
||
|
- collisions: option declarations and definitions of different modules can collide accidentally.
|
||
|
- unlimited access: A module can read and write to arbitrary fields of arbitrary other modules by default. This can result in unwanted side effects and hidden dependencies on other modules. Even if a module doesn't declare a dependency on module X it can depend on module X setting some option of module Y correctly. These unwanted interactions can be very complex and hard to find and prevent.
|
||
|
|
||
|
Considered workaround:
|
||
|
We established the following pattern:
|
||
|
|
||
|
- Each module prefixes all its options with the modules name, for example:
|
||
|
- module `mkDerivation` defines options `mkDerivation.src` and `mkDerivation.buildPhase`
|
||
|
- module `buildPythonPackages` defines options `buildPythonPackage.format` ...
|
||
|
|
||
|
Benefit of the workaround:
|
||
|
This prevents collisions (assuming module names are unique)
|
||
|
|
||
|
Disadvantage of the workaround:
|
||
|
|
||
|
- It still allows global read/write access between all modules.
|
||
|
- It prevents composition of interfaces: We cannot nicely mix the options of `mkDerivation` and `buildPythonPackage` to create a new module, as all options have a hardcoded prefix that cannot be changed anymore
|
||
|
- Using the module as submodule is more annoying, as because of the hardcoded prefix, it always adds an additional layer of nesting that might not be desired.
|
||
|
|
||
|
## Proposal 1
|
||
|
|
||
|
Solves:
|
||
|
|
||
|
- Problem 1 (Bad control over module dependencies)
|
||
|
- Problem 2 (Module identification/duplication issues)
|
||
|
|
||
|
Proposed Changes:
|
||
|
|
||
|
- generally separate `dependency declaration` from `dependency satisfaction`
|
||
|
- for example, add a flag to evalModules that changes the behavior of `imports`
|
||
|
- force modules to declare `imports` by name (never by path)
|
||
|
- have a `resolver` resolving requested names against a set of named modules provided by the user
|
||
|
- allow inspecting the requested dependencies before evaluation
|
||
|
|
||
|
Effects:
|
||
|
|
||
|
- users can discover module dependencies
|
||
|
- users can override the resolved module and thereby replace it's implementation
|
||
|
- maintainers can discover and prevent hidden dependencies easily
|
||
|
- lay the grounds for better input management (derive flake inputs from modules)
|
||
|
|
||
|
## Proposal 2
|
||
|
|
||
|
Solves:
|
||
|
|
||
|
- Problem 3 (Result type and location not discoverable)
|
||
|
|
||
|
Proposed Changes:
|
||
|
|
||
|
- standardize a specific field under config to contain the final result(s), like for example `config.exports`.
|
||
|
|
||
|
Effects:
|
||
|
|
||
|
- the result type is discoverable by inspecting the type of `options.exports`
|
||
|
- allows adding helper `callModule` which is like `evalModules` but just returns the result.
|
||
|
- allows users to treat modules like functions that can be called and return a result.
|
||
|
- modules are more approachable by high-level users
|
||
|
- modules are more portable.
|
||
|
|
||
|
## Proposal 3
|
||
|
|
||
|
Solves:
|
||
|
|
||
|
- Problem 4 (Unprotected global scope)
|
||
|
|
||
|
Proposed Changes:
|
||
|
|
||
|
- disallow nested option declarations
|
||
|
- disallow inline definitions for submodules
|
||
|
|
||
|
(This limitation could be toggled via a flag in evalModules)
|
||
|
|
||
|
Effects:
|
||
|
|
||
|
- This forces maintainers to use submodules (defined in files) to create nested options
|
||
|
- This leads to an extensive use of submodules
|
||
|
- Using submodules encourages passing information explicitly between modules while discouraging the use of global fields for communication
|