From 6b00c1c026d0231a4b51361c422c1f1b06fc2e22 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Thu, 13 Jul 2023 18:11:36 +0200 Subject: [PATCH 01/36] Draft module system introduction tutorial --- source/tutorials/index.md | 1 + source/tutorials/module-system/geocode | 53 + source/tutorials/module-system/icat | 3 + source/tutorials/module-system/map | 60 + .../tutorials/module-system/module-system.md | 1027 +++++++++++++++++ 5 files changed, 1144 insertions(+) create mode 100755 source/tutorials/module-system/geocode create mode 100755 source/tutorials/module-system/icat create mode 100755 source/tutorials/module-system/map create mode 100644 source/tutorials/module-system/module-system.md diff --git a/source/tutorials/index.md b/source/tutorials/index.md index 45dca52bff..7f3940d32e 100644 --- a/source/tutorials/index.md +++ b/source/tutorials/index.md @@ -12,4 +12,5 @@ nix-language.md learning-journey/index.md nixos/index.md cross-compilation.md +module-system/module-system.md ``` diff --git a/source/tutorials/module-system/geocode b/source/tutorials/module-system/geocode new file mode 100755 index 0000000000..2da35ab2da --- /dev/null +++ b/source/tutorials/module-system/geocode @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +cachedir=~/.cache/google-api/geocode +mkdir -p "$cachedir" +hash=$(echo "$1" | sha256sum - | cut -d' ' -f1) +cachefile="$cachedir/$hash" + +if [[ ! -f "$cachefile" ]]; then + + keyFile=${XDG_DATA_HOME:-~/.local/share}/google-api/key + + if [[ ! -f "$keyFile" ]]; then + mkdir -p "$(basename "$keyFile")" + echo "No Google API key found in $keyFile" >&2 + echo "For getting one, see https://developers.google.com/maps/documentation/geocoding/overview#before-you-begin" >&2 + exit 1 + fi + + key=$(cat "$keyFile") + + + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' exit + + output=$tmp/output + + curlArgs=( + https://maps.googleapis.com/maps/api/geocode/json + --silent --show-error --get --output "$output" --write-out '%{http_code}' + --data-urlencode address="$1" + ) + + #echo curl ''${curlArgs[@]@Q} >&2 + + curlArgs+=(--data-urlencode key="$key") + + if status=$(curl "${curlArgs[@]}"); then + if [[ "$status" == 200 ]]; then + jq -r '.results[0].geometry.location as $loc | "\($loc | .lat),\($loc | .lng)"' "$output" > "$cachefile" + else + echo "API returned non-200 HTTP status code $status, output is" >&2 + cat "$output" >&2 + exit 1 + fi + else + code=$? + echo "curl exited with code $code" >&2 + exit 1 + fi +fi + +cat "$cachefile" diff --git a/source/tutorials/module-system/icat b/source/tutorials/module-system/icat new file mode 100755 index 0000000000..cf71093132 --- /dev/null +++ b/source/tutorials/module-system/icat @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +feh - diff --git a/source/tutorials/module-system/map b/source/tutorials/module-system/map new file mode 100755 index 0000000000..70ae5a83eb --- /dev/null +++ b/source/tutorials/module-system/map @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +cachedir=${XDG_CACHE_HOME:-~/.cache}/google-api/maps-static +mkdir -p "$cachedir" +hash=$(echo "$@" | sha256sum - | cut -d' ' -f1) +cachefile="$cachedir/$hash" + +if [[ ! -f "$cachefile" ]]; then + + keyFile=${XDG_DATA_HOME:-~/.local/share}/google-api/key + + if [[ ! -f "$keyFile" ]]; then + mkdir -p "$(basename "$keyFile")" + echo "No Google API key found in $keyFile" >&2 + echo "For getting one, see https://developers.google.com/maps/documentation/maps-static/start#before-you-begin" >&2 + exit 1 + fi + + key=$(cat "$keyFile") + + + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' exit + + output=$tmp/output + + curlArgs=( + https://maps.googleapis.com/maps/api/staticmap + --silent --show-error --get --output "$output" --write-out %{http_code} + ) + + for arg in "$@"; do + curlArgs+=(--data-urlencode "$arg") + done + + #echo curl ''${curlArgs[@]@Q} >&2 + + curlArgs+=(--data-urlencode key="$key") + + if status=$(curl "${curlArgs[@]}"); then + if [[ "$status" == 200 ]]; then + mv "$output" "$cachefile" + else + echo "API returned non-200 HTTP status code $status, output is" >&2 + cat "$output" >&2 + exit 1 + fi + else + code=$? + echo "curl exited with code $code" >&2 + exit 1 + fi +fi + +if [[ -t 1 ]]; then + echo "Successful, but won't output image to tty, pipe to a file or icat instead" >&2 +else + cat "$cachefile" +fi diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md new file mode 100644 index 0000000000..8aa0d45419 --- /dev/null +++ b/source/tutorials/module-system/module-system.md @@ -0,0 +1,1027 @@ +# Module system introduction + +Note: This tutorial was created from https://github.com/tweag/summer-of-nix-modules, as presented by @infinisil at Summer of Nix 2021, presentation can be seen here (might not work in browser, `mpv` should work): https://infinisil.com/modules.mp4 + +## Empty module + +The simplest module you can have is just an empty attribute set, which +as you might expect, doesn't do anything! + +```diff +diff --git a/default.nix b/default.nix +new file mode 100644 +index 0000000..1797133 +--- /dev/null ++++ b/default.nix +@@ -0,0 +1,3 @@ ++{ ++ ++} +``` + +## Module arguments: lib + +Modules can be just an attribute set. But if you want access to some +arguments you need to change it into a function taking an attribute set +with an ellipsis (that's the "..."). In this case we only match on the +`lib` argument, which gives us access to nixpkgs library functions. + +Note that the ellipsis is necessary since arbitrary arguments can be +passed to modules. + +```diff +diff --git a/default.nix b/default.nix +index 1797133..f7569bd 100644 +--- a/default.nix ++++ b/default.nix +@@ -1,3 +1,3 @@ +-{ ++{ lib, ... }: { + + } +``` + +## Declaring generate.script option + +In order for modules to be useful, we need to have options, so let's +declare one. Options are declared by defining an attribute with +`lib.mkOption` under the `options` attribute. Here we're defining the +option `generate.script`. + +While there are many attributes to customize options, the most +important one is `type`, which specifies what values are valid for an +option, and how/whether multiple values should be merged together. + +In the nixpkgs library there are a number of types available under +`lib.types`. Here we're using the `lines` type, which specifies that: +- Only strings are valid values +- Multiple strings are joined with newlines + +```diff +diff --git a/default.nix b/default.nix +index f7569bd..cb423a9 100644 +--- a/default.nix ++++ b/default.nix +@@ -1,3 +1,9 @@ + { lib, ... }: { ++ ++ options = { ++ generate.script = lib.mkOption { ++ type = lib.types.lines; ++ }; ++ }; + + } +``` + +Let's try to evaluate this with this file: + +```nix +# eval.nix +(import ).evalModules { + modules = [ + ./default.nix + ]; +} +``` + +Then we run +```bash +nix-instantiate --eval eval.nix -A config.generate.script +``` + +Trying to evaluate the `generate.script` option however, we get an error +that the option is used but not defined, indicating that we need to +actually give a value to the option. + + +## Type checking: Assigning integer to generate.script + +We can try to assign an integer to our option of type `lines`, but the +module system correctly throws an error saying that our definition +doesn't match the options type. + +```diff +diff --git a/default.nix b/default.nix +index cb423a9..0a9162e 100644 +--- a/default.nix ++++ b/default.nix +@@ -5,5 +5,9 @@ + type = lib.types.lines; + }; + }; ++ ++ config = { ++ generate.script = 42; ++ }; + + } +``` + +## Successful evaluation: Assigning a string to generate.script + +We can make type checking pass by assigning a string to the option, +giving us our first successful evaluation of the `generate.script` +option. + +In this case, we assign a script which calls the Google Maps Static API +to generate a world map, then displaying the result using icat +(image-cat), both of which are helper scripts. + +```diff +diff --git a/default.nix b/default.nix +index 0a9162e..24f9c34 100644 +--- a/default.nix ++++ b/default.nix +@@ -7,7 +7,9 @@ + }; + + config = { +- generate.script = 42; ++ generate.script = '' ++ map size=640x640 scale=2 | icat ++ ''; + }; + + } +``` + +TODO: Create derivations to get these commands + +## A new list option: Declaring generate.requestParams + +Let's introduce another option, generate.requestParams. For its type, +we'll use `listOf `, which is a generic list type where each +element has to match the given nested type. In our case we want `str` to +be the nested type, which is a generic string type. + +Note that the difference between `str` and `lines` is in their merging +behavior: +- For `lines`, multiple definitions get merged by concatenation with + newlines +- For `str`, multiple definitions are not allowed. Which in this case is + mostly irrelevant however, since we can't really define a list element + multiple times. + +```diff +diff --git a/default.nix b/default.nix +index 24f9c34..fd5027a 100644 +--- a/default.nix ++++ b/default.nix +@@ -4,12 +4,21 @@ + generate.script = lib.mkOption { + type = lib.types.lines; + }; ++ ++ generate.requestParams = lib.mkOption { ++ type = lib.types.listOf lib.types.str; ++ }; + }; + + config = { + generate.script = '' + map size=640x640 scale=2 | icat + ''; ++ ++ generate.requestParams = [ ++ "size=640x640" ++ "scale=2" ++ ]; + }; + + } +``` + +## Dependencies between options: Using generate.requestParams + +A collection of modules generally only has a single option that is meant +to be evaluated. That's the option that generates the final result we're +interested in, which in our case is `generate.script`. + +In order to build up abstractions on that, we have the ability for +options to depend on other options. In this case we want the +`generate.script` option to use the values of `generate.requestParams`. +We can access the values of options by adding the `config` argument to +the argument list at the top and using e.g. +`config.generate.requestParams` to access that options value. + +We're then using `lib.concatStringsSep " "` to join each list element +of the option together into an argument list. + +```diff +diff --git a/default.nix b/default.nix +index fd5027a..050a98e 100644 +--- a/default.nix ++++ b/default.nix +@@ -1,4 +1,4 @@ +-{ lib, ... }: { ++{ lib, config, ... }: { + + options = { + generate.script = lib.mkOption { +@@ -12,7 +12,9 @@ + + config = { + generate.script = '' +- map size=640x640 scale=2 | icat ++ map ${lib.concatStringsSep " " ++ config.generate.requestParams ++ } | icat + ''; + + generate.requestParams = [ +``` + +## Conditional definitions: Introducing map.zoom option + +We now introduce the new option `map.zoom` in order to control the zoom +level of the map. We'll use a new type for it, `nullOr `, which +accepts the value `null`, but also values of its argument type. We're +using `null` here to mean an inferred zoom level. + +For this option, we'll set a default value which should be used if it's +not defined otherwise, which we can do using `mkOption`'s `default` +argument. + +Now we want to use this option to define another element in +`generate.requestParams`, but we only want to add this element if its +value is non-null. We can do this using the `mkIf +` function, which only adds a definition if the condition +holds. + +```diff +diff --git a/default.nix b/default.nix +index 050a98e..6b6e1e1 100644 +--- a/default.nix ++++ b/default.nix +@@ -8,6 +8,13 @@ + generate.requestParams = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; ++ ++ map = { ++ zoom = lib.mkOption { ++ type = lib.types.nullOr lib.types.int; ++ default = 2; ++ }; ++ }; + }; + + config = { +@@ -20,6 +27,8 @@ + generate.requestParams = [ + "size=640x640" + "scale=2" ++ (lib.mkIf (config.map.zoom != null) ++ "zoom=${toString config.map.zoom}") + ]; + }; + +``` + +## Declaring map.center option + +Similarly, let's declare a map.center option, declaring where the map +should be centered. + +We'll be using a small utility for geocoding location names, aka turning +them from names into coordinates + +```diff +diff --git a/default.nix b/default.nix +index 6b6e1e1..098d135 100644 +--- a/default.nix ++++ b/default.nix +@@ -14,6 +14,11 @@ + type = lib.types.nullOr lib.types.int; + default = 2; + }; ++ ++ center = lib.mkOption { ++ type = lib.types.nullOr lib.types.str; ++ default = "switzerland"; ++ }; + }; + }; + +@@ -29,6 +34,10 @@ + "scale=2" + (lib.mkIf (config.map.zoom != null) + "zoom=${toString config.map.zoom}") ++ (lib.mkIf (config.map.center != null) ++ "center=\"$(geocode ${ ++ lib.escapeShellArg config.map.center ++ })\"") + ]; + }; + +``` + +## Splitting modules: importing marker.nix + +The module system allows you to split your config into multiple files +via the `imports` attribute, which can define further modules to import. +This allows us to logically separate options and config for different +parts. + +Here we create a new module `marker.nix`, where we will declare options +for defining markers on the map + +```diff +diff --git a/default.nix b/default.nix +index 098d135..9d9f29d 100644 +--- a/default.nix ++++ b/default.nix +@@ -1,5 +1,9 @@ + { lib, config, ... }: { + ++ imports = [ ++ ./marker.nix ++ ]; ++ + options = { + generate.script = lib.mkOption { + type = lib.types.lines; +diff --git a/marker.nix b/marker.nix +new file mode 100644 +index 0000000..035c28d +--- /dev/null ++++ b/marker.nix +@@ -0,0 +1,3 @@ ++{ lib, config, ... }: { ++ ++} +``` + +## Submodule types: Declaring map.markers option + +One of the most useful types of the module system is the `submodule` +type. This type allows you to define a nested module system evaluation, +with its own options. Every value of such a type is then interpreted +(by default) as a `config` assignment of the nested module evaluation. + +In this case we're defining a `map.markers` option, whose type is a list +of submodules with a nested `location` type, allowing us to define a +list of markers on the map, where each assignment is type checked +according to the submodule. + +```diff +diff --git a/marker.nix b/marker.nix +index 035c28d..6a6c686 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -1,3 +1,20 @@ +-{ lib, config, ... }: { ++{ lib, config, ... }: ++let ++ ++ markerType = lib.types.submodule { ++ options = { ++ location = lib.mkOption { ++ type = lib.types.nullOr lib.types.str; ++ default = null; ++ }; ++ }; ++ }; ++in { ++ ++ options = { ++ map.markers = lib.mkOption { ++ type = lib.types.listOf markerType; ++ }; ++ }; + + } +``` + +## Single option namespace: Defining markers in generate.requestParams + +Since all modules in `imports` are treated the same, we can also freely +assign an option defined in our initial module. In this case we want to +add some request parameters derived from the `map.markers` option, so +that markers actually show up on the map. + +```diff +diff --git a/marker.nix b/marker.nix +index 6a6c686..c2c5da2 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -17,4 +17,26 @@ in { + }; + }; + ++ config = { ++ ++ map.markers = [ ++ { location = "new york"; } ++ ]; ++ ++ generate.requestParams = let ++ paramForMarker = marker: ++ let ++ attributes = ++ [ ++ "$(geocode ${ ++ lib.escapeShellArg marker.location ++ })" ++ ]; ++ in "markers=${ ++ lib.concatStringsSep "\\|" attributes ++ }"; ++ in map paramForMarker config.map.markers; ++ ++ }; ++ + } +``` + +## Set map.{center,zoom} for more than {1,2} markers + +Let's let the API infer the center if we have more than one marker, and +let's let it infer the zoom as well if there's more than 2. + +```diff +diff --git a/marker.nix b/marker.nix +index c2c5da2..b80ddd0 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -23,6 +23,14 @@ in { + { location = "new york"; } + ]; + ++ map.center = lib.mkIf ++ (lib.length config.map.markers >= 1) ++ null; ++ ++ map.zoom = lib.mkIf ++ (lib.length config.map.markers >= 2) ++ null; ++ + generate.requestParams = let + paramForMarker = marker: + let +``` + +## Nested submodules: Introducing users option + +We will now introduce the option `users`, where we make use of a very +useful type, `lib.types.attrsOf `. This type lets us specify an +attribute set as a value, where the keys can be arbitrary, but each +value has to conform to the given . + +In this case, we'll use another submodule as the subtype, one that +allows declaring a departure marker, which notably also makes use of our +`markerType` submodule, giving us a nested structure of submodules. + +We're now propagating each of the users marker definitions to the +`map.markers` option. + +```diff +diff --git a/marker.nix b/marker.nix +index b80ddd0..3c54ad8 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -9,9 +9,24 @@ let + }; + }; + }; ++ ++ userType = lib.types.submodule { ++ options = { ++ departure = lib.mkOption { ++ type = markerType; ++ default = {}; ++ }; ++ }; ++ }; ++ + in { + + options = { ++ ++ users = lib.mkOption { ++ type = lib.types.attrsOf userType; ++ }; ++ + map.markers = lib.mkOption { + type = lib.types.listOf markerType; + }; +@@ -19,9 +34,11 @@ in { + + config = { + +- map.markers = [ +- { location = "new york"; } +- ]; ++ map.markers = lib.filter ++ (marker: marker.location != null) ++ (lib.concatMap (user: [ ++ user.departure ++ ]) (lib.attrValues config.users)); + + map.center = lib.mkIf + (lib.length config.map.markers >= 1) +``` + +## Introducing style.label and strMatching type + +Let's add an option to customize markers with a label. We can do so by +just adding another option in our markerType submodule. The API states +that this has to be an uppercase letter or a number, which we can +implement with the `strMatching ""` type. + +```diff +diff --git a/marker.nix b/marker.nix +index 3c54ad8..1c9a043 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -7,6 +7,12 @@ let + type = lib.types.nullOr lib.types.str; + default = null; + }; ++ ++ style.label = lib.mkOption { ++ type = lib.types.nullOr ++ (lib.types.strMatching "[A-Z0-9]"); ++ default = null; ++ }; + }; + }; + +@@ -52,7 +58,10 @@ in { + paramForMarker = marker: + let + attributes = +- [ ++ lib.optional ++ (marker.style.label != null) ++ "label:${marker.style.label}" ++ ++ [ + "$(geocode ${ + lib.escapeShellArg marker.location + })" +``` + +## Using the attribute name in the submodule to define a default label + +Let's set a default label by deriving it from the username. By +transforming the submodule's argument into a function, we can access +arguments within it. One special argument available to submodules is the +`name` argument, which when used in `attrsOf`, gives you the name of the +attribute the submodule is defined under. + +In this case, we don't easily have access to the name from the marker +submodules label option (where we could set a `default =`). Instead we +will use the `config` section of the user submodule to set a default. We +can do so using the `lib.mkDefault` modifier, which has lower precedence +than if no modifier were used. + +```diff +diff --git a/marker.nix b/marker.nix +index 1c9a043..53860f1 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -1,5 +1,11 @@ + { lib, config, ... }: + let ++ # Returns the uppercased first letter ++ # or number of a string ++ firstUpperAlnum = str: ++ lib.mapNullable lib.head ++ (builtins.match "[^A-Z0-9]*([A-Z0-9]).*" ++ (lib.toUpper str)); + + markerType = lib.types.submodule { + options = { +@@ -16,14 +22,19 @@ let + }; + }; + +- userType = lib.types.submodule { ++ userType = lib.types.submodule ({ name, ... }: { + options = { + departure = lib.mkOption { + type = markerType; + default = {}; + }; + }; +- }; ++ ++ config = { ++ departure.style.label = lib.mkDefault ++ (firstUpperAlnum name); ++ }; ++ }); + + in { + +``` + +## Marker colors + +Let's allow markers to change their color as well. We'll use some new +type functions for this, namely +- `either `: Takes two types as arguments, allows either of + them +- `enum [ ]`: Takes a list of allowed values + +```diff +diff --git a/marker.nix b/marker.nix +index 53860f1..df0d08b 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -7,6 +7,13 @@ let + (builtins.match "[^A-Z0-9]*([A-Z0-9]).*" + (lib.toUpper str)); + ++ # Either a color name or `0xRRGGBB` ++ colorType = lib.types.either ++ (lib.types.strMatching "0x[0-9A-F]{6}") ++ (lib.types.enum [ ++ "black" "brown" "green" "purple" "yellow" ++ "blue" "gray" "orange" "red" "white" ]); ++ + markerType = lib.types.submodule { + options = { + location = lib.mkOption { +@@ -19,6 +26,11 @@ let + (lib.types.strMatching "[A-Z0-9]"); + default = null; + }; ++ ++ style.color = lib.mkOption { ++ type = colorType; ++ default = "red"; ++ }; + }; + }; + +@@ -73,6 +85,7 @@ in { + (marker.style.label != null) + "label:${marker.style.label}" + ++ [ ++ "color:${marker.style.color}" + "$(geocode ${ + lib.escapeShellArg marker.location + })" +``` + +## Marker size + +Let's also allow changing of marker sizes. + +```diff +diff --git a/marker.nix b/marker.nix +index df0d08b..2c0c1a8 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -31,6 +31,12 @@ let + type = colorType; + default = "red"; + }; ++ ++ style.size = lib.mkOption { ++ type = lib.types.enum ++ [ "tiny" "small" "medium" "large" ]; ++ default = "medium"; ++ }; + }; + }; + +@@ -80,10 +86,20 @@ in { + generate.requestParams = let + paramForMarker = marker: + let ++ size = { ++ tiny = "tiny"; ++ small = "small"; ++ medium = "mid"; ++ large = null; ++ }.${marker.style.size}; ++ + attributes = + lib.optional + (marker.style.label != null) + "label:${marker.style.label}" ++ ++ lib.optional ++ (size != null) ++ "size:${size}" + ++ [ + "color:${marker.style.color}" + "$(geocode ${ +``` + +## Initial path module + +Let's introduce a new module for declaring paths on the map. We'll +import a new `path.nix` module from our `marker.nix` module. + +In the path module we'll define an option for declaring paths, and will +use the same `generate.requestParams` to influence the API call to +include our defined paths + +```diff +diff --git a/marker.nix b/marker.nix +index 2c0c1a8..ffb8185 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -56,6 +56,10 @@ let + + in { + ++ imports = [ ++ ./path.nix ++ ]; ++ + options = { + + users = lib.mkOption { +diff --git a/path.nix b/path.nix +new file mode 100644 +index 0000000..554a88b +--- /dev/null ++++ b/path.nix +@@ -0,0 +1,32 @@ ++{ lib, config, ... }: ++let ++ pathType = lib.types.submodule { ++ ++ options = { ++ locations = lib.mkOption { ++ type = lib.types.listOf lib.types.str; ++ }; ++ }; ++ ++ }; ++in { ++ options = { ++ map.paths = lib.mkOption { ++ type = lib.types.listOf pathType; ++ }; ++ }; ++ ++ config = { ++ generate.requestParams = let ++ attrForLocation = loc: ++ "$(geocode ${lib.escapeShellArg loc})"; ++ paramForPath = path: ++ let ++ attributes = ++ map attrForLocation path.locations; ++ in "path=${ ++ lib.concatStringsSep "\\|" attributes ++ }"; ++ in map paramForPath config.map.paths; ++ }; ++} +``` + +## Arrival marker + +Now in order for users to be able to draw a path in their definitions, +we need to allow them to specify another marker. We will copy the +`departure` option declaration to a new `arrival` option for that. + +```diff +diff --git a/marker.nix b/marker.nix +index ffb8185..940b8f8 100644 +--- a/marker.nix ++++ b/marker.nix +@@ -46,11 +46,18 @@ let + type = markerType; + default = {}; + }; ++ ++ arrival = lib.mkOption { ++ type = markerType; ++ default = {}; ++ }; + }; + + config = { + departure.style.label = lib.mkDefault + (firstUpperAlnum name); ++ arrival.style.label = lib.mkDefault ++ (firstUpperAlnum name); + }; + }); + +@@ -76,7 +83,7 @@ in { + map.markers = lib.filter + (marker: marker.location != null) + (lib.concatMap (user: [ +- user.departure ++ user.departure user.arrival + ]) (lib.attrValues config.users)); + + map.center = lib.mkIf +``` + +## Connecting user paths + +In our path module, we can now define a path spanning from every users +departure location to their arrival location. + +```diff +diff --git a/path.nix b/path.nix +index 554a88b..d4a3a84 100644 +--- a/path.nix ++++ b/path.nix +@@ -17,6 +17,17 @@ in { + }; + + config = { ++ ++ map.paths = map (user: { ++ locations = [ ++ user.departure.location ++ user.arrival.location ++ ]; ++ }) (lib.filter (user: ++ user.departure.location != null ++ && user.arrival.location != null ++ ) (lib.attrValues config.users)); ++ + generate.requestParams = let + attrForLocation = loc: + "$(geocode ${lib.escapeShellArg loc})"; +``` + +## Introducing path weight option + +Let's also allow some customization of path styles with a `weight` option. +As already done before, we'll declare a submodule for the path style. + +While we could also directly define the style.weight option in this +case, we will use the submodule in a future change to reuse the path +style definitions. + +Note how we're using a new type for this, `ints.between +`, which allows integers in the given inclusive range. + +```diff +diff --git a/path.nix b/path.nix +index d4a3a84..88766a8 100644 +--- a/path.nix ++++ b/path.nix +@@ -1,11 +1,26 @@ + { lib, config, ... }: + let ++ ++ pathStyleType = lib.types.submodule { ++ options = { ++ weight = lib.mkOption { ++ type = lib.types.ints.between 1 20; ++ default = 5; ++ }; ++ }; ++ }; ++ + pathType = lib.types.submodule { + + options = { + locations = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; ++ ++ style = lib.mkOption { ++ type = pathStyleType; ++ default = {}; ++ }; + }; + + }; +@@ -34,7 +49,10 @@ in { + paramForPath = path: + let + attributes = +- map attrForLocation path.locations; ++ [ ++ "weight:${toString path.style.weight}" ++ ] ++ ++ map attrForLocation path.locations; + in "path=${ + lib.concatStringsSep "\\|" attributes + }"; +``` + +## User path styles + +Now users can't actually customize the path style yet, so let's +introduce a new `pathStyle` option for each user. + +But wait! Didn't we already define the `user` option in the `marker.nix` +module? Yes we did, but the module system actually allows us to declare +an option multiple times, and the module system takes care of merging +each declarations types together (if possible). + +```diff +diff --git a/path.nix b/path.nix +index 88766a8..8b56782 100644 +--- a/path.nix ++++ b/path.nix +@@ -26,6 +26,16 @@ let + }; + in { + options = { ++ ++ users = lib.mkOption { ++ type = lib.types.attrsOf (lib.types.submodule { ++ options.pathStyle = lib.mkOption { ++ type = pathStyleType; ++ default = {}; ++ }; ++ }); ++ }; ++ + map.paths = lib.mkOption { + type = lib.types.listOf pathType; + }; +@@ -38,6 +48,7 @@ in { + user.departure.location + user.arrival.location + ]; ++ style = user.pathStyle; + }) (lib.filter (user: + user.departure.location != null + && user.arrival.location != null +``` + +## Introducing path color option + +Very similar to markers, let's allow customization of the path color, +using types we've seen before already. + +```diff +diff --git a/path.nix b/path.nix +index 8b56782..d2073fe 100644 +--- a/path.nix ++++ b/path.nix +@@ -1,12 +1,25 @@ + { lib, config, ... }: + let + ++ # Either a color name, `0xRRGGBB` or `0xRRGGBBAA` ++ colorType = lib.types.either ++ (lib.types.strMatching "0x[0-9A-F]{6}[0-9A-F]{2}?") ++ (lib.types.enum [ ++ "black" "brown" "green" "purple" "yellow" ++ "blue" "gray" "orange" "red" "white" ++ ]); ++ + pathStyleType = lib.types.submodule { + options = { + weight = lib.mkOption { + type = lib.types.ints.between 1 20; + default = 5; + }; ++ ++ color = lib.mkOption { ++ type = colorType; ++ default = "blue"; ++ }; + }; + }; + +@@ -62,6 +75,7 @@ in { + attributes = + [ + "weight:${toString path.style.weight}" ++ "color:${path.style.color}" + ] + ++ map attrForLocation path.locations; + in "path=${ +``` + +## Introducing geodesic path option + +Finally, another option for the path style, using a new but very simple +type, `bool`, which just allows `true` and `false`. + +```diff +diff --git a/path.nix b/path.nix +index d2073fe..ebd9561 100644 +--- a/path.nix ++++ b/path.nix +@@ -20,6 +20,11 @@ let + type = colorType; + default = "blue"; + }; ++ ++ geodesic = lib.mkOption { ++ type = lib.types.bool; ++ default = false; ++ }; + }; + }; + +@@ -76,6 +81,7 @@ in { + [ + "weight:${toString path.style.weight}" + "color:${path.style.color}" ++ "geodesic:${lib.boolToString path.style.geodesic}" + ] + ++ map attrForLocation path.locations; + in "path=${ +``` + From 1a00a42dee248e5fcaf269028221f57483b57845 Mon Sep 17 00:00:00 2001 From: Alexander Groleau Date: Thu, 14 Sep 2023 13:11:45 -0500 Subject: [PATCH 02/36] add intro/conclusion, rework prose, follow styleguide, clean diffs --- .../tutorials/module-system/module-system.md | 881 +++++++++--------- 1 file changed, 456 insertions(+), 425 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 8aa0d45419..096deb3094 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -1,80 +1,83 @@ -# Module system introduction +# The Module System +Much of the power in Nixpkgs comes from the module system, which provides mechanisms for automatically merging attribute sets, making it easy to compose configurations in a type-safe way. -Note: This tutorial was created from https://github.com/tweag/summer-of-nix-modules, as presented by @infinisil at Summer of Nix 2021, presentation can be seen here (might not work in browser, `mpv` should work): https://infinisil.com/modules.mp4 +In this tutorial, you'll write your first modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. + +You'll learn what a module is and how to define one, what options are and how to declare them, how to express dependencies between modules, and a practical way to use Nix to wrap external APIs. + +Be prepared to see some Nix errors: during the tutorial, you will first write some *incorrect* configurations, creating opportunities to discuss the resulting error messages and how to resolve them, particularly when discussing type checking. + +:::{note} +This tutorial follows [@infinisil's presentation](https://infinisil.com/modules.mp4) of the [Summer of Nix Modules](https://github.com/tweag/summer-of-nix-modules), during the 2021 Summer of Nix. +::: ## Empty module -The simplest module you can have is just an empty attribute set, which -as you might expect, doesn't do anything! +The simplest module is just an empty attribute set, which doesn't do anything! -```diff -diff --git a/default.nix b/default.nix -new file mode 100644 -index 0000000..1797133 ---- /dev/null -+++ b/default.nix -@@ -0,0 +1,3 @@ -+{ -+ -+} +Write the following into a file called `default.nix`: + +```nix +# default.nix +{ + +} ``` -## Module arguments: lib +## Module Arguments -Modules can be just an attribute set. But if you want access to some -arguments you need to change it into a function taking an attribute set -with an ellipsis (that's the "..."). In this case we only match on the -`lib` argument, which gives us access to nixpkgs library functions. +In order to make a module actually useful, you will need to write it as a *function*, which takes an attribute set as an argument. -Note that the ellipsis is necessary since arbitrary arguments can be -passed to modules. +Do this by adding the following new line to `default.nix`: ```diff -diff --git a/default.nix b/default.nix -index 1797133..f7569bd 100644 ---- a/default.nix -+++ b/default.nix -@@ -1,3 +1,3 @@ --{ -+{ lib, ... }: { - - } +# default.nix ++ { lib, ... }: +{ + +} ``` -## Declaring generate.script option +The addition of this line turns the expression in `default.nix` into a function which takes *at least* one argument, called `lib`, and may accept other arguments (expressed by the ellipsis `...`). + +Matching on the `lib` argument will make `nixpkgs` library functions available within the function body. + +:::{note} +The ellipsis `...` is necessary because arbitrary arguments can be passed to modules. +::: + +## Declaring Options -In order for modules to be useful, we need to have options, so let's -declare one. Options are declared by defining an attribute with -`lib.mkOption` under the `options` attribute. Here we're defining the -option `generate.script`. +One of the reasons for writing modules is to declare names which can be assigned values and used in other computations elsewhere. -While there are many attributes to customize options, the most -important one is `type`, which specifies what values are valid for an -option, and how/whether multiple values should be merged together. +For your new module to become useful, you will need to add some *options*, which define these named-values. -In the nixpkgs library there are a number of types available under -`lib.types`. Here we're using the `lines` type, which specifies that: -- Only strings are valid values -- Multiple strings are joined with newlines +Options are declared by defining an attribute under the top-level `options` attribute, using `lib.mkOption`. + +In this section, you will define the `generate.script` option. + +Change `default.nix` to include the following declaration: ```diff -diff --git a/default.nix b/default.nix -index f7569bd..cb423a9 100644 ---- a/default.nix -+++ b/default.nix -@@ -1,3 +1,9 @@ +# default.nix { lib, ... }: { -+ -+ options = { -+ generate.script = lib.mkOption { -+ type = lib.types.lines; -+ }; -+ }; - + ++ options = { ++ generate.script = lib.mkOption { ++ type = lib.types.lines; ++ }; ++ }; + } ``` -Let's try to evaluate this with this file: +While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option, and how or whether multiple values should be merged together. + +There are several other types available under [`lib.types`](https://github.com/NixOS/nixpkgs/blob/master/lib/types.nix) in the nixpkgs library. + +You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple strings should be joined with newlines. + +Write a new file, `eval.nix`, which you will use to evaluate `default.nix`: ```nix # eval.nix @@ -85,90 +88,87 @@ Let's try to evaluate this with this file: } ``` -Then we run +Now execute the following command: + ```bash nix-instantiate --eval eval.nix -A config.generate.script ``` -Trying to evaluate the `generate.script` option however, we get an error -that the option is used but not defined, indicating that we need to -actually give a value to the option. +You will see an error message indicating that the `generate.script` option is used but not defined; you will need to assign a value to the option before using it. + +## Type Checking +As previously mentioned, the `lines` type only permits string values. +:::{warning} +In this section, you will make your first type error. Be prepared! +::: -## Type checking: Assigning integer to generate.script +What happens if you instead try to assign an integer to the option? -We can try to assign an integer to our option of type `lines`, but the -module system correctly throws an error saying that our definition -doesn't match the options type. +Add the following lines to `default.nix`: ```diff -diff --git a/default.nix b/default.nix -index cb423a9..0a9162e 100644 ---- a/default.nix -+++ b/default.nix -@@ -5,5 +5,9 @@ - type = lib.types.lines; - }; - }; -+ -+ config = { -+ generate.script = 42; -+ }; - +# default.nix + { lib, ... }: { + + options = { + generate.script = lib.mkOption { + type = lib.types.lines; + }; + }; + ++ config = { ++ generate.script = 42; ++ }; } ``` -## Successful evaluation: Assigning a string to generate.script +Now try to execute the previous command, and witness your first module error: + +```console +$ nix-instantiate --eval eval.nix -A config.generate.script +error: +... + error: A definition for option `generate.script' is not of type `strings concatenated with "\n"'. Definition values: + - In `/home/nix-user/default.nix': 42 +``` + +This assignment of `generate.script = 42;` caused a type error: integers are not strings concatenated with the newline character. -We can make type checking pass by assigning a string to the option, -giving us our first successful evaluation of the `generate.script` -option. +## Successful Type-checking -In this case, we assign a script which calls the Google Maps Static API -to generate a world map, then displaying the result using icat -(image-cat), both of which are helper scripts. +To make this module pass the type-checker and successfully evaluate the `generate.script` option, you will now assign a string to `generate.script`. + +In this case, you will assign a `map` script which first calls the Google Maps Static API to generate a world map, then displays the result using `icat` (image-cat), both of which are helper scripts. + +Update `default.nix` by changing the value of `generate.script` to the following string: ```diff -diff --git a/default.nix b/default.nix -index 0a9162e..24f9c34 100644 ---- a/default.nix -+++ b/default.nix -@@ -7,7 +7,9 @@ - }; - +# default.nix config = { - generate.script = 42; + generate.script = '' + map size=640x640 scale=2 | icat + ''; }; - - } ``` TODO: Create derivations to get these commands -## A new list option: Declaring generate.requestParams +## Declaring More Options +In this section, you will introduce another option: `generate.requestParams`. + +For its type, you should use `listOf `, which is a generic list type where each element must have the given nested type. -Let's introduce another option, generate.requestParams. For its type, -we'll use `listOf `, which is a generic list type where each -element has to match the given nested type. In our case we want `str` to -be the nested type, which is a generic string type. +Instead of `lines`, in this case you will want the nested type to be `str`, a generic string type. -Note that the difference between `str` and `lines` is in their merging -behavior: -- For `lines`, multiple definitions get merged by concatenation with - newlines -- For `str`, multiple definitions are not allowed. Which in this case is - mostly irrelevant however, since we can't really define a list element - multiple times. +The difference between `str` and `lines` is in their merging behavior: +- For `lines`, multiple definitions get merged by concatenation with newlines. +- For `str`, multiple definitions are not allowed. This is mostly irrelevant here however, since it is not really possible to define a list element multiple times. +Make the following additions to your `default.nix` file now: ```diff -diff --git a/default.nix b/default.nix -index 24f9c34..fd5027a 100644 ---- a/default.nix -+++ b/default.nix -@@ -4,12 +4,21 @@ +# default.nix generate.script = lib.mkOption { type = lib.types.lines; }; @@ -177,7 +177,7 @@ index 24f9c34..fd5027a 100644 + type = lib.types.listOf lib.types.str; + }; }; - + config = { generate.script = '' map size=640x640 scale=2 | icat @@ -188,39 +188,36 @@ index 24f9c34..fd5027a 100644 + "scale=2" + ]; }; - + } ``` -## Dependencies between options: Using generate.requestParams +## Dependencies Between Options -A collection of modules generally only has a single option that is meant -to be evaluated. That's the option that generates the final result we're -interested in, which in our case is `generate.script`. +A given module generally only declares a single option that is meant to be evaluated. -In order to build up abstractions on that, we have the ability for -options to depend on other options. In this case we want the -`generate.script` option to use the values of `generate.requestParams`. -We can access the values of options by adding the `config` argument to -the argument list at the top and using e.g. -`config.generate.requestParams` to access that options value. +This option generates the final result to be used elsewhere, which in this case is `generate.script`. -We're then using `lib.concatStringsSep " "` to join each list element -of the option together into an argument list. +Options have the ability to depend on other options, making it possible to build more useful abstractions. +Here, the plan is for the `generate.script` option to use the values of `generate.requestParams` as arguments to the `map` command. + +### Accessing Option Values +To make a declared option available, the argument attribute set of the module declaring it must include the `config` attribute. + +Update `default.nix` to add the `config` attribute: ```diff -diff --git a/default.nix b/default.nix -index fd5027a..050a98e 100644 ---- a/default.nix -+++ b/default.nix -@@ -1,4 +1,4 @@ +# default.nix -{ lib, ... }: { +{ lib, config, ... }: { - - options = { - generate.script = lib.mkOption { -@@ -12,7 +12,9 @@ - +``` + +When a module declaring an option is evaluated, values of the resulting option can be accessed by using attribute names to access the corresponding values. + +Now make the following changes to `default.nix`: + +```diff +# default.nix config = { generate.script = '' - map size=640x640 scale=2 | icat @@ -228,33 +225,32 @@ index fd5027a..050a98e 100644 + config.generate.requestParams + } | icat ''; - - generate.requestParams = [ ``` -## Conditional definitions: Introducing map.zoom option +Here, the value of the `config.generate.requestParams` attribute is substituted at its call site. -We now introduce the new option `map.zoom` in order to control the zoom -level of the map. We'll use a new type for it, `nullOr `, which -accepts the value `null`, but also values of its argument type. We're -using `null` here to mean an inferred zoom level. +`lib.concatStringsSep " "` is then used to join each list element from the value of `config.generate.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. -For this option, we'll set a default value which should be used if it's -not defined otherwise, which we can do using `mkOption`'s `default` -argument. +The result of this represents the list of command line arguments to pass to `map`. -Now we want to use this option to define another element in -`generate.requestParams`, but we only want to add this element if its -value is non-null. We can do this using the `mkIf -` function, which only adds a definition if the condition -holds. +## Conditional Definitions and Default Values + +In this section, you will define a new option, `map.zoom`, to control the zoom level of the map. + +You will use a new type, `nullOr `, which can take as values either the values of its argument type or `null`. + +In this case, a `null` value will use the API's default behavior of inferring the zoom level. + +Here, you will also use `default` from `mkOption`](https://github.com/NixOS/nixpkgs/blob/master/lib/options.nix) to declare your first *default* value, which will be used if the option declaring it is not enabled. + +You will use this option to define another element in `generate.requestParams`, which will only be added if its value is non-null. + +To do this, you can use the `mkIf ` function, which only adds the definition if the condition holds. + +Add the `map` attribute set with the `zoom` option into the top-level `options` declaration, like so: ```diff -diff --git a/default.nix b/default.nix -index 050a98e..6b6e1e1 100644 ---- a/default.nix -+++ b/default.nix -@@ -8,6 +8,13 @@ +# default.nix generate.requestParams = lib.mkOption { type = lib.types.listOf lib.types.str; }; @@ -266,9 +262,14 @@ index 050a98e..6b6e1e1 100644 + }; + }; }; - + ``` + +Now make the following additions to the `generate.requestParams` list in the `config` block: + +```diff +# default.nix config = { -@@ -20,6 +27,8 @@ + ... generate.requestParams = [ "size=640x640" "scale=2" @@ -276,23 +277,15 @@ index 050a98e..6b6e1e1 100644 + "zoom=${toString config.map.zoom}") ]; }; - ``` -## Declaring map.center option +## Centering the Map -Similarly, let's declare a map.center option, declaring where the map -should be centered. - -We'll be using a small utility for geocoding location names, aka turning -them from names into coordinates +You have now declared options controlling the map dimensions and zoom level, but have not provided a way to specify where the map should be centered. +Add the `center` option now, possibly with your own location as default value: ```diff -diff --git a/default.nix b/default.nix -index 6b6e1e1..098d135 100644 ---- a/default.nix -+++ b/default.nix -@@ -14,6 +14,11 @@ +# default.nix type = lib.types.nullOr lib.types.int; default = 2; }; @@ -303,8 +296,14 @@ index 6b6e1e1..098d135 100644 + }; }; }; - -@@ -29,6 +34,10 @@ +``` + +To implement this behavior, you will use the `geocode` utility, which turns location names into coordinates. + +Add another `mkIf` call to the list of `requestParams` now: + +```diff +# default.nix "scale=2" (lib.mkIf (config.map.zoom != null) "zoom=${toString config.map.zoom}") @@ -314,63 +313,52 @@ index 6b6e1e1..098d135 100644 + })\"") ]; }; - ``` -## Splitting modules: importing marker.nix +This time, you've used `escapeShellArg` to pass the `config.map.center` value as a command-line argument to `geocode`, interpolating the result back into the `requestParams` string which sets the `center` value. + +Wrapping shell command execution in Nix modules is a powerful technique for controlling system changes using the ergnomic attributes and values interface. + +## Splitting Modules + +The module schema includes the `imports` attribute, which allows you to define further modules to import, enabling a *modular* approach where your configuration may be split into multiple files. -The module system allows you to split your config into multiple files -via the `imports` attribute, which can define further modules to import. -This allows us to logically separate options and config for different -parts. +In particular, this allows you to separate option declarations from their call-sites in your configuration. -Here we create a new module `marker.nix`, where we will declare options -for defining markers on the map +You should now create a new module, `marker.nix`, where you can declare options for defining location pins and other markers on the map. +```diff +# marker.nix ++{ lib, config, ... }: { ++ ++} +``` +Reference this new file in `default.nix` using the `imports` attribute: ```diff -diff --git a/default.nix b/default.nix -index 098d135..9d9f29d 100644 ---- a/default.nix -+++ b/default.nix -@@ -1,5 +1,9 @@ +# default.nix { lib, config, ... }: { - + + imports = [ + ./marker.nix + ]; + - options = { - generate.script = lib.mkOption { - type = lib.types.lines; -diff --git a/marker.nix b/marker.nix -new file mode 100644 -index 0000000..035c28d ---- /dev/null -+++ b/marker.nix -@@ -0,0 +1,3 @@ -+{ lib, config, ... }: { -+ -+} ``` -## Submodule types: Declaring map.markers option +## The `submodule` Type + +One of the most useful types included in the module system's type system is `submodule`. + +This type allows you to define nested modules with their own options. -One of the most useful types of the module system is the `submodule` -type. This type allows you to define a nested module system evaluation, -with its own options. Every value of such a type is then interpreted -(by default) as a `config` assignment of the nested module evaluation. +Every value of such a type is then interpreted (by default) as a `config` assignment of the nested module evaluation. -In this case we're defining a `map.markers` option, whose type is a list -of submodules with a nested `location` type, allowing us to define a -list of markers on the map, where each assignment is type checked -according to the submodule. +Here, you will define a new `map.markers` option whose type is a list of submodules, each with a nested `location` type, allowing you to define a list of markers on the map. +Each assignment of markers will be type-checked during evaluation of the top-level `config`. + +Make the following changes to `marker.nix` now: ```diff -diff --git a/marker.nix b/marker.nix -index 035c28d..6a6c686 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -1,3 +1,20 @@ +# marker.nix -{ lib, config, ... }: { +{ lib, config, ... }: +let @@ -390,26 +378,19 @@ index 035c28d..6a6c686 100644 + type = lib.types.listOf markerType; + }; + }; - } ``` -## Single option namespace: Defining markers in generate.requestParams +## Setting Option Values Within Other Modules + +Because of the way the module system composes option definitions, you can also freely assign values to options defined in other modules. -Since all modules in `imports` are treated the same, we can also freely -assign an option defined in our initial module. In this case we want to -add some request parameters derived from the `map.markers` option, so -that markers actually show up on the map. +In this case, you will use the `map.markers` option to derive and add new `requestParams`, making your declared markers appear on the returned map. +To implement this behavior, add the following `config` block to `marker.nix`: ```diff -diff --git a/marker.nix b/marker.nix -index 6a6c686..c2c5da2 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -17,4 +17,26 @@ in { - }; - }; - +# marker.nix + ... + config = { + + map.markers = [ @@ -431,24 +412,20 @@ index 6a6c686..c2c5da2 100644 + in map paramForMarker config.map.markers; + + }; -+ } ``` -## Set map.{center,zoom} for more than {1,2} markers +Here, you again used `escapeShellArg` and string interpolation to generate a Nix string, this time producing a pipe-separated list of geocoded location attributes. + +The `generate.requestParams` value was also set to the resulting list of strings, which gets appended to the `generate.requestParams` list defined in `default.nix`, thanks to the default behavior of the `list`-type module. + +## Multiple Markers -Let's let the API infer the center if we have more than one marker, and -let's let it infer the zoom as well if there's more than 2. +In case you define multiple markers, determining an appropriate center or zoom level for the map may be challenging; it's easier to let the API do this for you. +To do this, make the following additions to `marker.nix`, above the `generate.requestParams` declaration: ```diff -diff --git a/marker.nix b/marker.nix -index c2c5da2..b80ddd0 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -23,6 +23,14 @@ in { - { location = "new york"; } - ]; - +# marker.nix + map.center = lib.mkIf + (lib.length config.map.markers >= 1) + null; @@ -462,30 +439,24 @@ index c2c5da2..b80ddd0 100644 let ``` -## Nested submodules: Introducing users option +In this case, the default behavior of the Maps API when not passed a center or zoom level is to pick the geometric center of all the given markers, and to set a zoom level appropriate for viewing all markers at once. + +## Nested Submodules + +It's time to introduce the `users` option with the `lib.types.attrsOf ` type, which will allow you to define `users` as an attribute set with arbitrary keys, each value of which has type ``. -We will now introduce the option `users`, where we make use of a very -useful type, `lib.types.attrsOf `. This type lets us specify an -attribute set as a value, where the keys can be arbitrary, but each -value has to conform to the given . +Here, that subtype will be another submodule which allows declaring a departure marker, suitable for querying the API for the recommended route for a trip. -In this case, we'll use another submodule as the subtype, one that -allows declaring a departure marker, which notably also makes use of our -`markerType` submodule, giving us a nested structure of submodules. +This will also make use of the `markerType` submodule, giving a nested structure of submodules. -We're now propagating each of the users marker definitions to the -`map.markers` option. +To propagate marker definitions from `users` to the `map.markers` option, make the following changes now: + +- In the `let` block: ```diff -diff --git a/marker.nix b/marker.nix -index b80ddd0..3c54ad8 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -9,9 +9,24 @@ let - }; - }; - }; -+ +# marker.nix + let + ... + userType = lib.types.submodule { + options = { + departure = lib.mkOption { @@ -496,20 +467,24 @@ index b80ddd0..3c54ad8 100644 + }; + in { - +``` + +- In the `options` block, above `map.markers`: +```diff +# marker.nix options = { + + users = lib.mkOption { + type = lib.types.attrsOf userType; + }; + - map.markers = lib.mkOption { - type = lib.types.listOf markerType; - }; -@@ -19,9 +34,11 @@ in { - +``` + +- In the `config` block, above `map.center`: +```diff +# marker.nix config = { - + - map.markers = [ - { location = "new york"; } - ]; @@ -518,24 +493,36 @@ index b80ddd0..3c54ad8 100644 + (lib.concatMap (user: [ + user.departure + ]) (lib.attrValues config.users)); - + map.center = lib.mkIf (lib.length config.map.markers >= 1) ``` -## Introducing style.label and strMatching type +The `config.users` attribute set is passed to `attrValues`, which returns a list of values of each of the attributes in the set (here, the set of `config.users` you've defined), sorted alphabetically. + +The `departure` values of each of the `users` are then joined into a list, and this list is filtered for non-`null` locations. + +The resulting list is stored in `map.markers`. + +The resulting `map.markers` option then propagates to the `generate.requestParams` option, which in turn is used to generate arguments to the script which ultimately calls the Maps API. + +Defining the options in this way allows you to set multiple `users..departure.location` values and generate a map with the appropriate zoom and center, with pins corresponding to the set of `departure.location` values for *all* `users`. + +In the 2021 Summer of Nix, this formed the basis of an interactive multi-person map demo. + +## Labeling Markers + +Now that the map can be rendered with multiple markers, it's time to add some style customization. -Let's add an option to customize markers with a label. We can do so by -just adding another option in our markerType submodule. The API states -that this has to be an uppercase letter or a number, which we can -implement with the `strMatching ""` type. +To tell the markers apart, you should add another option to the `markerType` submodule, to allow labeling each marker pin. +The API [states](https://developers.google.com/maps/documentation/maps-static/start#MarkerStyles) that these labels must be either an uppercase letter or a number. + +You can implement this with the `strMatching ""` type, where `` is a regular expression used for the matching; this will reject any unacceptable (non-uppercase letter or number) values. + +- In the `let` block: ```diff -diff --git a/marker.nix b/marker.nix -index 3c54ad8..1c9a043 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -7,6 +7,12 @@ let +# marker.nix type = lib.types.nullOr lib.types.str; default = null; }; @@ -547,8 +534,11 @@ index 3c54ad8..1c9a043 100644 + }; }; }; - -@@ -52,7 +58,10 @@ in { +``` + +- In the `paramForMarker` function: +```diff +# marker.nix paramForMarker = marker: let attributes = @@ -562,27 +552,19 @@ index 3c54ad8..1c9a043 100644 })" ``` -## Using the attribute name in the submodule to define a default label +Here, the label for each `marker` is only propagated to the CLI parameters if `marker.style.label` is set. + +## Defining a Default Label + +In case you don't want to manually define a label for every marker, you can set a default value. -Let's set a default label by deriving it from the username. By -transforming the submodule's argument into a function, we can access -arguments within it. One special argument available to submodules is the -`name` argument, which when used in `attrsOf`, gives you the name of the -attribute the submodule is defined under. +The easiest value for a default label is the username, which will always also be set. -In this case, we don't easily have access to the name from the marker -submodules label option (where we could set a `default =`). Instead we -will use the `config` section of the user submodule to set a default. We -can do so using the `lib.mkDefault` modifier, which has lower precedence -than if no modifier were used. +This `firstUpperAlnum` function allows you to retrieve the first character of the username, with the correct type for passing to `departure.style.label`: ```diff -diff --git a/marker.nix b/marker.nix -index 1c9a043..53860f1 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -1,5 +1,11 @@ - { lib, config, ... }: +# marker.nix +{ lib, config, ... }: let + # Returns the uppercased first letter + # or number of a string @@ -590,13 +572,19 @@ index 1c9a043..53860f1 100644 + lib.mapNullable lib.head + (builtins.match "[^A-Z0-9]*([A-Z0-9]).*" + (lib.toUpper str)); - + markerType = lib.types.submodule { options = { -@@ -16,14 +22,19 @@ let - }; - }; - +``` + +By transforming the argument to `lib.types.submodule` into a function, you can access arguments within it. + +One special argument available to submodules is the `name` argument, which when used in `attrsOf`, gives you the name of the attribute the submodule is defined under. + +You can use this function argument to retrieve the `name` attribute for use elsewhere: + +```diff +# marker.nix - userType = lib.types.submodule { + userType = lib.types.submodule ({ name, ... }: { options = { @@ -606,34 +594,49 @@ index 1c9a043..53860f1 100644 }; }; - }; +``` + +In this case, you don't easily have access to the name from the marker submodules `label` option, where you otherwise could set a `default` value. + +Instead you can use the `config` section of the `user` submodule to set a default, like so: + +```diff +# marker.nix + + config = { + departure.style.label = lib.mkDefault + (firstUpperAlnum name); + }; + }); - + in { - + ``` -## Marker colors +:::{note} +Module options have a *precedence*, represented as an integer, which determines the priority of setting the option to a particular value. + +The `lib.mkDefault` modifier sets the precedence of its argument value to 1000, the lowest priority. + +This ensures that other values set for the same option will prevail. +::: + + +## Marker Styling: Color -Let's allow markers to change their color as well. We'll use some new -type functions for this, namely -- `either `: Takes two types as arguments, allows either of - them -- `enum [ ]`: Takes a list of allowed values +For better visual contrast, it would also be helpful to have a way to change the *color* of a marker. +Here you will use two new type-functions for this: +- `either `, which takes two types as arguments, allows either of them +- `enum [ ]`, which takes a list of allowed values + +In the `let` block, add the following `colorType` option, which can hold strings containing either some given color names or an RGB value: ```diff -diff --git a/marker.nix b/marker.nix -index 53860f1..df0d08b 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -7,6 +7,13 @@ let +# marker.nix + ... (builtins.match "[^A-Z0-9]*([A-Z0-9]).*" (lib.toUpper str)); - + + # Either a color name or `0xRRGGBB` + colorType = lib.types.either + (lib.types.strMatching "0x[0-9A-F]{6}") @@ -644,7 +647,11 @@ index 53860f1..df0d08b 100644 markerType = lib.types.submodule { options = { location = lib.mkOption { -@@ -19,6 +26,11 @@ let +``` + +At the bottom of the `let` block, add the `style.color` option: +```diff +# marker.nix (lib.types.strMatching "[A-Z0-9]"); default = null; }; @@ -655,8 +662,11 @@ index 53860f1..df0d08b 100644 + }; }; }; - -@@ -73,6 +85,7 @@ in { +``` + +Now add a line to the `paramForMarker` list which makes use of the new option: +```diff +# marker.nix (marker.style.label != null) "label:${marker.style.label}" ++ [ @@ -666,16 +676,14 @@ index 53860f1..df0d08b 100644 })" ``` -## Marker size +## Marker Styling: Size + +In case you set many different markers, it would be helpful to have the ability to change their size individually, further improving visual accessibility. -Let's also allow changing of marker sizes. +Add a new `style.size` option to `marker.nix`, allowing you to do so: ```diff -diff --git a/marker.nix b/marker.nix -index df0d08b..2c0c1a8 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -31,6 +31,12 @@ let +# marker.nix type = colorType; default = "red"; }; @@ -687,8 +695,11 @@ index df0d08b..2c0c1a8 100644 + }; }; }; - -@@ -80,10 +86,20 @@ in { +``` + +Now add a handler for the size parameter in `paramForMarker`, which selects an appropriate string to pass to the API: +```diff +# marker.nix generate.requestParams = let paramForMarker = marker: let @@ -699,6 +710,11 @@ index df0d08b..2c0c1a8 100644 + large = null; + }.${marker.style.size}; + +``` + +Finally, add another `lib.optional` call to the `attributes` string, making use of the selected size: +``` +# marker.nix attributes = lib.optional (marker.style.label != null) @@ -711,37 +727,16 @@ index df0d08b..2c0c1a8 100644 "$(geocode ${ ``` -## Initial path module +## The `pathType` Submodule + +So far, you've created an option for declaring a *destination* marker, as well as several options for configuring the marker's visual representation. -Let's introduce a new module for declaring paths on the map. We'll -import a new `path.nix` module from our `marker.nix` module. +The new option defined in the next section will allow you to set an *arrival* marker, which together with a destination allows you to draw *paths* on the map using the new module defined below. -In the path module we'll define an option for declaring paths, and will -use the same `generate.requestParams` to influence the API call to -include our defined paths +To start, create a new `path.nix` file with the following contents: ```diff -diff --git a/marker.nix b/marker.nix -index 2c0c1a8..ffb8185 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -56,6 +56,10 @@ let - - in { - -+ imports = [ -+ ./path.nix -+ ]; -+ - options = { - - users = lib.mkOption { -diff --git a/path.nix b/path.nix -new file mode 100644 -index 0000000..554a88b ---- /dev/null -+++ b/path.nix -@@ -0,0 +1,32 @@ +# path.nix +{ lib, config, ... }: +let + pathType = lib.types.submodule { @@ -776,18 +771,29 @@ index 0000000..554a88b +} ``` -## Arrival marker +The `path.nix` module defines an option for declaring paths, augmenting the API call by re-using the `generate.requestParams` option. -Now in order for users to be able to draw a path in their definitions, -we need to allow them to specify another marker. We will copy the -`departure` option declaration to a new `arrival` option for that. +Now import this new `path.nix` module from your `marker.nix` module: ```diff -diff --git a/marker.nix b/marker.nix -index ffb8185..940b8f8 100644 ---- a/marker.nix -+++ b/marker.nix -@@ -46,11 +46,18 @@ let +# marker.nix + in { + ++ imports = [ ++ ./path.nix ++ ]; ++ + options = { + + users = lib.mkOption { +``` + +## The Arrival Marker + +Now copy the `departure` option declaration to a new `arrival` option in `marker.nix`, to complete the initial path implementation: + +```diff +# marker.nix type = markerType; default = {}; }; @@ -797,7 +803,11 @@ index ffb8185..940b8f8 100644 + default = {}; + }; }; - +``` + +Next, add an `arrival.style.label` attribute to the `config` block, mirroring the `departure.style.label`: +```diff +# marker.nix config = { departure.style.label = lib.mkDefault (firstUpperAlnum name); @@ -805,31 +815,29 @@ index ffb8185..940b8f8 100644 + (firstUpperAlnum name); }; }); - -@@ -76,7 +83,7 @@ in { +``` + +Finally, update the return list in the function passed to `concatMap` in `map.markers`: +```diff +# marker.nix map.markers = lib.filter (marker: marker.location != null) (lib.concatMap (user: [ - user.departure + user.departure user.arrival ]) (lib.attrValues config.users)); - + map.center = lib.mkIf ``` -## Connecting user paths +You should now be able to define paths on the map, connecting pairs of departure and arrival points. + +## Connecting Markers by Paths -In our path module, we can now define a path spanning from every users -departure location to their arrival location. +In the path module, you can now define a path connecting every user's departure and arrival locations. ```diff -diff --git a/path.nix b/path.nix -index 554a88b..d4a3a84 100644 ---- a/path.nix -+++ b/path.nix -@@ -17,6 +17,17 @@ in { - }; - +# path.nix config = { + + map.paths = map (user: { @@ -847,24 +855,25 @@ index 554a88b..d4a3a84 100644 "$(geocode ${lib.escapeShellArg loc})"; ``` -## Introducing path weight option +:::{warning} +Don't confuse the `map` function with the `map` option or the `map` script! +::: + +The new `map.paths` attribute contains a list of all valid paths defined for all users. -Let's also allow some customization of path styles with a `weight` option. -As already done before, we'll declare a submodule for the path style. +A path is valid only if the `departure` and `arrival` attributes are set for that user. -While we could also directly define the style.weight option in this -case, we will use the submodule in a future change to reuse the path -style definitions. +## Path Styling: Weight -Note how we're using a new type for this, `ints.between -`, which allows integers in the given inclusive range. +Your users have spoken, and they demand the ability to customize the styles of their paths with a `weight` option. +As before, you'll now declare a new submodule for the path style. + +While you could also directly define the `style.weight` option, in this case, you should use the submodule in a future change to reuse the path style definitions. + +Add the `pathStyleType` submodule option to the `let` block in `path.nix`: ```diff -diff --git a/path.nix b/path.nix -index d4a3a84..88766a8 100644 ---- a/path.nix -+++ b/path.nix -@@ -1,11 +1,26 @@ +# path.nix { lib, config, ... }: let + @@ -878,7 +887,17 @@ index d4a3a84..88766a8 100644 + }; + pathType = lib.types.submodule { - +``` + +:::{note} +The `ints.between ` type allows integers in the given (inclusive) range. +::: + +The path weight will default to 5, but can be set to any integer value in the 1 to 20 range, with higher weights producing thicker paths on the map. + +Now add a `style` option to the `options` set further down the file: +```diff +# path.nix options = { locations = lib.mkOption { type = lib.types.listOf lib.types.str; @@ -889,9 +908,13 @@ index d4a3a84..88766a8 100644 + default = {}; + }; }; - + }; -@@ -34,7 +49,10 @@ in { +``` + +Finally, update the `attributes` list in `paramForPath`: +```diff +# path.nix paramForPath = path: let attributes = @@ -905,23 +928,16 @@ index d4a3a84..88766a8 100644 }"; ``` -## User path styles +## The `pathStyle` Submodule + +Users still can't actually customize the path style yet, so you should introduce a new `pathStyle` option for each user. -Now users can't actually customize the path style yet, so let's -introduce a new `pathStyle` option for each user. +The module system allows you to declare values for an option multiple times, and if the types permit doing so, takes care of merging each declaration's values together. -But wait! Didn't we already define the `user` option in the `marker.nix` -module? Yes we did, but the module system actually allows us to declare -an option multiple times, and the module system takes care of merging -each declarations types together (if possible). +This makes it possible to have a definition for the `user` option in the `marker.nix` module, as well as a `user` definition in `path.nix`, which you should add now: ```diff -diff --git a/path.nix b/path.nix -index 88766a8..8b56782 100644 ---- a/path.nix -+++ b/path.nix -@@ -26,6 +26,16 @@ let - }; +# path.nix in { options = { + @@ -937,7 +953,11 @@ index 88766a8..8b56782 100644 map.paths = lib.mkOption { type = lib.types.listOf pathType; }; -@@ -38,6 +48,7 @@ in { +``` + +Then add a line using the `user.pathStyle` option in `map.paths`: +```diff +# path.nix user.departure.location user.arrival.location ]; @@ -947,20 +967,18 @@ index 88766a8..8b56782 100644 && user.arrival.location != null ``` -## Introducing path color option +## Path Styling: Color + +As with markers, paths should have customizable colors. -Very similar to markers, let's allow customization of the path color, -using types we've seen before already. +You can accomplish this using types you've already seen by now. +Add a new `colorType` block to `path.nix`, specifying the allowed color names and RGB/RGBA values: ```diff -diff --git a/path.nix b/path.nix -index 8b56782..d2073fe 100644 ---- a/path.nix -+++ b/path.nix -@@ -1,12 +1,25 @@ +# path.nix { lib, config, ... }: let - + + # Either a color name, `0xRRGGBB` or `0xRRGGBBAA` + colorType = lib.types.either + (lib.types.strMatching "0x[0-9A-F]{6}[0-9A-F]{2}?") @@ -970,8 +988,11 @@ index 8b56782..d2073fe 100644 + ]); + pathStyleType = lib.types.submodule { - options = { - weight = lib.mkOption { +``` + +Under the `weight` option, add a new `color` option to use the new `colorType` value: +```diff +# path.nix type = lib.types.ints.between 1 20; default = 5; }; @@ -982,8 +1003,11 @@ index 8b56782..d2073fe 100644 + }; }; }; - -@@ -62,6 +75,7 @@ in { +``` + +Finally, add a line using the `color` option to the `attributes` list: +```diff +# path.nix attributes = [ "weight:${toString path.style.weight}" @@ -993,17 +1017,15 @@ index 8b56782..d2073fe 100644 in "path=${ ``` -## Introducing geodesic path option +## Further Styling + +To further improve the aesthetics of the rendered map, you should add another style option allowing paths to be drawn as *geodesics*, the shortest "as the crow flies" distance between two points on Earth. -Finally, another option for the path style, using a new but very simple -type, `bool`, which just allows `true` and `false`. +Since this feature can be turned on or off, you can do this using the `bool` type, which can be `true` or `false`. +Make the following changes to `path.nix` now: ```diff -diff --git a/path.nix b/path.nix -index d2073fe..ebd9561 100644 ---- a/path.nix -+++ b/path.nix -@@ -20,6 +20,11 @@ let +# path.nix type = colorType; default = "blue"; }; @@ -1014,8 +1036,11 @@ index d2073fe..ebd9561 100644 + }; }; }; - -@@ -76,6 +81,7 @@ in { +``` + +Make sure to also add a new line using this to the `attributes` list, so the option value is included in the API call: +```diff +# path.nix [ "weight:${toString path.style.weight}" "color:${path.style.color}" @@ -1025,3 +1050,9 @@ index d2073fe..ebd9561 100644 in "path=${ ``` +## Wrapping Up +In this tutorial, you've learned how to write custom Nix modules to bring external services under declarative control, with the help of several new utility functions from the Nixpkgs `lib`. + +You defined several modules in multiple files, each with separate submodules making use of the module system's type checking. + +These modules exposed features of the external API in a declarative way. From c5dbed11a54e2cfd5cd53b76a82cfdf832159bca Mon Sep 17 00:00:00 2001 From: Alexander Groleau Date: Thu, 14 Sep 2023 16:24:18 -0500 Subject: [PATCH 03/36] Review pass Co-authored-by: Valentin Gagarin --- .../tutorials/module-system/module-system.md | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 096deb3094..25d6db34a7 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -1,24 +1,26 @@ -# The Module System -Much of the power in Nixpkgs comes from the module system, which provides mechanisms for automatically merging attribute sets, making it easy to compose configurations in a type-safe way. +# Deep dive demo: Wrapping the World in Modules +Much of the power in Nixpkgs and NixOS comes from the module system. +It provides mechanisms for conveniently declaring and automatically merging interdependent attribute sets that follow dynamic type constraints, making it easy to express modular configurations. -In this tutorial, you'll write your first modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. +In this tutorial you'll learn what a module is and how to define one, what options are and how to declare them, how to express dependencies between modules, and follow extensive demonstration of how to wrap an existing API with Nix modules. -You'll learn what a module is and how to define one, what options are and how to declare them, how to express dependencies between modules, and a practical way to use Nix to wrap external APIs. +Concretely, you'll write modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. Be prepared to see some Nix errors: during the tutorial, you will first write some *incorrect* configurations, creating opportunities to discuss the resulting error messages and how to resolve them, particularly when discussing type checking. :::{note} -This tutorial follows [@infinisil's presentation](https://infinisil.com/modules.mp4) of the [Summer of Nix Modules](https://github.com/tweag/summer-of-nix-modules), during the 2021 Summer of Nix. +This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation on modules](https://infinisil.com/modules.mp4) [(source)](https://github.com/tweag/summer-of-nix-modules) for 2021 Summer of Nix. ::: ## Empty module -The simplest module is just an empty attribute set, which doesn't do anything! +The simplest module is just a function that takes any attributes and returns empty attribute set. Write the following into a file called `default.nix`: ```nix # default.nix +{ ... }: { } @@ -48,9 +50,8 @@ The ellipsis `...` is necessary because arbitrary arguments can be passed to mod ## Declaring Options -One of the reasons for writing modules is to declare names which can be assigned values and used in other computations elsewhere. +Modules allow providing *options* that declare which values can be set and used elsewhere. -For your new module to become useful, you will need to add some *options*, which define these named-values. Options are declared by defining an attribute under the top-level `options` attribute, using `lib.mkOption`. @@ -71,9 +72,9 @@ Change `default.nix` to include the following declaration: } ``` -While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option, and how or whether multiple values should be merged together. +While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option. -There are several other types available under [`lib.types`](https://github.com/NixOS/nixpkgs/blob/master/lib/types.nix) in the nixpkgs library. +There are several other types available under [`lib.types`](https://github.com/NixOS/nixpkgs/blob/master/lib/types.nix) in the Nixpkgs library. You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple strings should be joined with newlines. @@ -88,7 +89,7 @@ Write a new file, `eval.nix`, which you will use to evaluate `default.nix`: } ``` -Now execute the following command: +Now run the following command: ```bash nix-instantiate --eval eval.nix -A config.generate.script @@ -100,7 +101,7 @@ You will see an error message indicating that the `generate.script` option is us As previously mentioned, the `lines` type only permits string values. :::{warning} -In this section, you will make your first type error. Be prepared! +In this section, you will set an invalid value and encounter a type error. ::: What happens if you instead try to assign an integer to the option? @@ -158,13 +159,14 @@ TODO: Create derivations to get these commands ## Declaring More Options In this section, you will introduce another option: `generate.requestParams`. -For its type, you should use `listOf `, which is a generic list type where each element must have the given nested type. +For its type, you should use `listOf `, which is a list type where each element must have the specified type. Instead of `lines`, in this case you will want the nested type to be `str`, a generic string type. The difference between `str` and `lines` is in their merging behavior: +Module option types not only check for valid values, but also specify how multiple definitions of an option are to be combined into one. - For `lines`, multiple definitions get merged by concatenation with newlines. -- For `str`, multiple definitions are not allowed. This is mostly irrelevant here however, since it is not really possible to define a list element multiple times. +- For `str`, multiple definitions are not allowed. This is not a problem here, since one can't define a list element multiple times. Make the following additions to your `default.nix` file now: ```diff @@ -198,12 +200,12 @@ A given module generally only declares a single option that is meant to be evalu This option generates the final result to be used elsewhere, which in this case is `generate.script`. -Options have the ability to depend on other options, making it possible to build more useful abstractions. +Options can depend on other options, making it possible to build more useful abstractions. -Here, the plan is for the `generate.script` option to use the values of `generate.requestParams` as arguments to the `map` command. +Here, we want the `generate.script` option to use the values of `generate.requestParams` as arguments to the `map` command. ### Accessing Option Values -To make a declared option available, the argument attribute set of the module declaring it must include the `config` attribute. +To make an option definition available, the argument of the module accessing it must include the `config` attribute. Update `default.nix` to add the `config` attribute: ```diff @@ -212,7 +214,7 @@ Update `default.nix` to add the `config` attribute: +{ lib, config, ... }: { ``` -When a module declaring an option is evaluated, values of the resulting option can be accessed by using attribute names to access the corresponding values. +When a module setting options is evaluated, these values can be accessed by their corresponding attribute names. Now make the following changes to `default.nix`: @@ -227,7 +229,8 @@ Now make the following changes to `default.nix`: ''; ``` -Here, the value of the `config.generate.requestParams` attribute is substituted at its call site. +Here, the value of the `config.generate.requestParams` attribute is populated by the module system based on the definitions in the same file. +This is possible due to lazy evaluation in the Nix language. `lib.concatStringsSep " "` is then used to join each list element from the value of `config.generate.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. @@ -237,7 +240,7 @@ The result of this represents the list of command line arguments to pass to `map In this section, you will define a new option, `map.zoom`, to control the zoom level of the map. -You will use a new type, `nullOr `, which can take as values either the values of its argument type or `null`. +You will use a new type, `nullOr `, which can take either values of its argument type or `null`. In this case, a `null` value will use the API's default behavior of inferring the zoom level. @@ -245,7 +248,7 @@ Here, you will also use `default` from `mkOption`](https://github.com/NixOS/nixp You will use this option to define another element in `generate.requestParams`, which will only be added if its value is non-null. -To do this, you can use the `mkIf ` function, which only adds the definition if the condition holds. +To do this, you can use the `mkIf ` function, which only adds the definition if the condition evaluates to `true`. Add the `map` attribute set with the `zoom` option into the top-level `options` declaration, like so: @@ -317,15 +320,15 @@ Add another `mkIf` call to the list of `requestParams` now: This time, you've used `escapeShellArg` to pass the `config.map.center` value as a command-line argument to `geocode`, interpolating the result back into the `requestParams` string which sets the `center` value. -Wrapping shell command execution in Nix modules is a powerful technique for controlling system changes using the ergnomic attributes and values interface. +Wrapping shell command execution in Nix modules is a helpful technique for controlling system changes, using the more ergonomic attributes and values interface rather than dealing with the peculiarities of escaping manually. ## Splitting Modules -The module schema includes the `imports` attribute, which allows you to define further modules to import, enabling a *modular* approach where your configuration may be split into multiple files. +The module schema includes the `imports` attribute, which allows incorporating further modules, for example to split a large configuration into multiple files. -In particular, this allows you to separate option declarations from their call-sites in your configuration. +In particular, this allows you to separate option declarations from where they are used in your configuration. -You should now create a new module, `marker.nix`, where you can declare options for defining location pins and other markers on the map. +Create a new module, `marker.nix`, where you can declare options for defining location pins and other markers on the map. ```diff # marker.nix +{ lib, config, ... }: { @@ -350,7 +353,6 @@ One of the most useful types included in the module system's type system is `sub This type allows you to define nested modules with their own options. -Every value of such a type is then interpreted (by default) as a `config` assignment of the nested module evaluation. Here, you will define a new `map.markers` option whose type is a list of submodules, each with a nested `location` type, allowing you to define a list of markers on the map. @@ -385,7 +387,7 @@ Make the following changes to `marker.nix` now: Because of the way the module system composes option definitions, you can also freely assign values to options defined in other modules. -In this case, you will use the `map.markers` option to derive and add new `requestParams`, making your declared markers appear on the returned map. +In this case, you will use the `map.markers` option to produce and add new elements to the `requestParams` list, making your declared markers appear on the returned map. To implement this behavior, add the following `config` block to `marker.nix`: ```diff @@ -417,7 +419,7 @@ To implement this behavior, add the following `config` block to `marker.nix`: Here, you again used `escapeShellArg` and string interpolation to generate a Nix string, this time producing a pipe-separated list of geocoded location attributes. -The `generate.requestParams` value was also set to the resulting list of strings, which gets appended to the `generate.requestParams` list defined in `default.nix`, thanks to the default behavior of the `list`-type module. +The `generate.requestParams` value was also set to the resulting list of strings, which gets appended to the `generate.requestParams` list defined in `default.nix`, thanks to the default merging behavior of the `list`-type module. ## Multiple Markers @@ -443,11 +445,11 @@ In this case, the default behavior of the Maps API when not passed a center or z ## Nested Submodules -It's time to introduce the `users` option with the `lib.types.attrsOf ` type, which will allow you to define `users` as an attribute set with arbitrary keys, each value of which has type ``. +It's time to introduce a `users` option with the `lib.types.attrsOf ` type, which will allow you to define `users` as an attribute set with arbitrary keys, each value of which has type ``. Here, that subtype will be another submodule which allows declaring a departure marker, suitable for querying the API for the recommended route for a trip. -This will also make use of the `markerType` submodule, giving a nested structure of submodules. +This will again make use of the `markerType` submodule, giving a nested structure of submodules. To propagate marker definitions from `users` to the `map.markers` option, make the following changes now: @@ -504,7 +506,7 @@ The `departure` values of each of the `users` are then joined into a list, and t The resulting list is stored in `map.markers`. -The resulting `map.markers` option then propagates to the `generate.requestParams` option, which in turn is used to generate arguments to the script which ultimately calls the Maps API. +The resulting `map.markers` option then propagates to the `generate.requestParams` option, which in turn is used to generate arguments to the script which ultimately calls the Google Maps API. Defining the options in this way allows you to set multiple `users..departure.location` values and generate a map with the appropriate zoom and center, with pins corresponding to the set of `departure.location` values for *all* `users`. @@ -516,9 +518,9 @@ Now that the map can be rendered with multiple markers, it's time to add some st To tell the markers apart, you should add another option to the `markerType` submodule, to allow labeling each marker pin. -The API [states](https://developers.google.com/maps/documentation/maps-static/start#MarkerStyles) that these labels must be either an uppercase letter or a number. +The API documentation states that [these labels must be either an uppercase letter or a number](https://developers.google.com/maps/documentation/maps-static/start#MarkerStyles). -You can implement this with the `strMatching ""` type, where `` is a regular expression used for the matching; this will reject any unacceptable (non-uppercase letter or number) values. +You can implement this with the `strMatching ""` type, where `` is a regular expression that will accept any matching (uppercase letter or number) values. - In the `let` block: ```diff @@ -627,8 +629,8 @@ This ensures that other values set for the same option will prevail. For better visual contrast, it would also be helpful to have a way to change the *color* of a marker. Here you will use two new type-functions for this: -- `either `, which takes two types as arguments, allows either of them -- `enum [ ]`, which takes a list of allowed values +- `either `, which takes two types as arguments, and allows either of them +- `enum [ ]`, which takes a list of allowed values, and allows any of them In the `let` block, add the following `colorType` option, which can hold strings containing either some given color names or an RGB value: ```diff @@ -678,7 +680,7 @@ Now add a line to the `paramForMarker` list which makes use of the new option: ## Marker Styling: Size -In case you set many different markers, it would be helpful to have the ability to change their size individually, further improving visual accessibility. +In case you set many different markers, it would be helpful to have the ability to change their size individually. Add a new `style.size` option to `marker.nix`, allowing you to do so: @@ -697,7 +699,7 @@ Add a new `style.size` option to `marker.nix`, allowing you to do so: }; ``` -Now add a handler for the size parameter in `paramForMarker`, which selects an appropriate string to pass to the API: +Now add a mapping for the size parameter in `paramForMarker`, which selects an appropriate string to pass to the API: ```diff # marker.nix generate.requestParams = let @@ -1019,7 +1021,7 @@ Finally, add a line using the `color` option to the `attributes` list: ## Further Styling -To further improve the aesthetics of the rendered map, you should add another style option allowing paths to be drawn as *geodesics*, the shortest "as the crow flies" distance between two points on Earth. +Now that you've got this far, to further improve the aesthetics of the rendered map, you should add another style option allowing paths to be drawn as *geodesics*, the shortest "as the crow flies" distance between two points on Earth. Since this feature can be turned on or off, you can do this using the `bool` type, which can be `true` or `false`. @@ -1056,3 +1058,5 @@ In this tutorial, you've learned how to write custom Nix modules to bring extern You defined several modules in multiple files, each with separate submodules making use of the module system's type checking. These modules exposed features of the external API in a declarative way. + +You can now conquer the world with Nix. From d87f5865d5fbc61a95b8b925c27ad4413ce57683 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 26 Oct 2023 21:04:15 +0200 Subject: [PATCH 04/36] Apply suggestions from code review Co-authored-by: asymmetric --- .../tutorials/module-system/module-system.md | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 25d6db34a7..328a020186 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -1,8 +1,14 @@ -# Deep dive demo: Wrapping the World in Modules +# Deep dive demo: Wrapping the world in modules Much of the power in Nixpkgs and NixOS comes from the module system. It provides mechanisms for conveniently declaring and automatically merging interdependent attribute sets that follow dynamic type constraints, making it easy to express modular configurations. -In this tutorial you'll learn what a module is and how to define one, what options are and how to declare them, how to express dependencies between modules, and follow extensive demonstration of how to wrap an existing API with Nix modules. +In this tutorial you'll learn +- what a module is +- how to define one +- what options are +- how to declare them +- how to express dependencies between modules +and follow extensive demonstration of how to wrap an existing API with Nix modules. Concretely, you'll write modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. @@ -14,7 +20,7 @@ This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation ## Empty module -The simplest module is just a function that takes any attributes and returns empty attribute set. +The simplest module is just a function that takes any attributes and returns an empty attribute set. Write the following into a file called `default.nix`: @@ -74,7 +80,7 @@ Change `default.nix` to include the following declaration: While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option. -There are several other types available under [`lib.types`](https://github.com/NixOS/nixpkgs/blob/master/lib/types.nix) in the Nixpkgs library. +There are several types available under [`lib.types`](https://github.com/NixOS/nixpkgs/blob/master/lib/types.nix) in the Nixpkgs library. You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple strings should be joined with newlines. @@ -161,14 +167,14 @@ In this section, you will introduce another option: `generate.requestParams`. For its type, you should use `listOf `, which is a list type where each element must have the specified type. -Instead of `lines`, in this case you will want the nested type to be `str`, a generic string type. +Instead of `lines`, in this case you will want the type of the list elements to be `str`, a generic string type. The difference between `str` and `lines` is in their merging behavior: Module option types not only check for valid values, but also specify how multiple definitions of an option are to be combined into one. - For `lines`, multiple definitions get merged by concatenation with newlines. - For `str`, multiple definitions are not allowed. This is not a problem here, since one can't define a list element multiple times. -Make the following additions to your `default.nix` file now: +Make the following additions to your `default.nix` file: ```diff # default.nix generate.script = lib.mkOption { @@ -214,7 +220,7 @@ Update `default.nix` to add the `config` attribute: +{ lib, config, ... }: { ``` -When a module setting options is evaluated, these values can be accessed by their corresponding attribute names. +When a module that sets options is evaluated, the resulting values can be accessed by their corresponding attribute names. Now make the following changes to `default.nix`: @@ -230,7 +236,7 @@ Now make the following changes to `default.nix`: ``` Here, the value of the `config.generate.requestParams` attribute is populated by the module system based on the definitions in the same file. -This is possible due to lazy evaluation in the Nix language. +Lazy evaluation in the Nix language allows taking a value from the `config` argument passed to the module which defines the value. `lib.concatStringsSep " "` is then used to join each list element from the value of `config.generate.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. @@ -324,7 +330,7 @@ Wrapping shell command execution in Nix modules is a helpful technique for contr ## Splitting Modules -The module schema includes the `imports` attribute, which allows incorporating further modules, for example to split a large configuration into multiple files. +The [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules) includes the `imports` attribute, which allows incorporating further modules, for example to split a large configuration into multiple files. In particular, this allows you to separate option declarations from where they are used in your configuration. @@ -441,11 +447,11 @@ To do this, make the following additions to `marker.nix`, above the `generate.re let ``` -In this case, the default behavior of the Maps API when not passed a center or zoom level is to pick the geometric center of all the given markers, and to set a zoom level appropriate for viewing all markers at once. +In this case, the default behavior of the Google Maps API when not passed a center or zoom level is to pick the geometric center of all the given markers, and to set a zoom level appropriate for viewing all markers at once. ## Nested Submodules -It's time to introduce a `users` option with the `lib.types.attrsOf ` type, which will allow you to define `users` as an attribute set with arbitrary keys, each value of which has type ``. +It's time to introduce a `users` option with type `lib.types.attrsOf `, which will allow you to define `users` as an attribute set, whose values have type ``. Here, that subtype will be another submodule which allows declaring a departure marker, suitable for querying the API for the recommended route for a trip. @@ -514,7 +520,7 @@ In the 2021 Summer of Nix, this formed the basis of an interactive multi-person ## Labeling Markers -Now that the map can be rendered with multiple markers, it's time to add some style customization. +Now that the map can be rendered with multiple markers, it's time to add some style customizations. To tell the markers apart, you should add another option to the `markerType` submodule, to allow labeling each marker pin. @@ -558,9 +564,9 @@ Here, the label for each `marker` is only propagated to the CLI parameters if `m ## Defining a Default Label -In case you don't want to manually define a label for every marker, you can set a default value. +In case you don't want to manually define a label for each marker, you can set a default value. -The easiest value for a default label is the username, which will always also be set. +The easiest value for a default label is the username, which will always be set. This `firstUpperAlnum` function allows you to retrieve the first character of the username, with the correct type for passing to `departure.style.label`: @@ -617,6 +623,7 @@ Instead you can use the `config` section of the `user` submodule to set a defaul :::{note} Module options have a *precedence*, represented as an integer, which determines the priority of setting the option to a particular value. +When merging values, the lowest precedence wins. The `lib.mkDefault` modifier sets the precedence of its argument value to 1000, the lowest priority. From 6fbd08fad12108954f91294c6ca5f699503f1bad Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 21:22:02 +0200 Subject: [PATCH 05/36] fix whitespace --- source/tutorials/module-system/module-system.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 328a020186..416f8dc031 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -1,4 +1,5 @@ # Deep dive demo: Wrapping the world in modules + Much of the power in Nixpkgs and NixOS comes from the module system. It provides mechanisms for conveniently declaring and automatically merging interdependent attribute sets that follow dynamic type constraints, making it easy to express modular configurations. @@ -57,8 +58,6 @@ The ellipsis `...` is necessary because arbitrary arguments can be passed to mod ## Declaring Options Modules allow providing *options* that declare which values can be set and used elsewhere. - - Options are declared by defining an attribute under the top-level `options` attribute, using `lib.mkOption`. In this section, you will define the `generate.script` option. @@ -79,7 +78,6 @@ Change `default.nix` to include the following declaration: ``` While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option. - There are several types available under [`lib.types`](https://github.com/NixOS/nixpkgs/blob/master/lib/types.nix) in the Nixpkgs library. You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple strings should be joined with newlines. @@ -163,6 +161,7 @@ Update `default.nix` by changing the value of `generate.script` to the following TODO: Create derivations to get these commands ## Declaring More Options + In this section, you will introduce another option: `generate.requestParams`. For its type, you should use `listOf `, which is a list type where each element must have the specified type. @@ -630,7 +629,6 @@ The `lib.mkDefault` modifier sets the precedence of its argument value to 1000, This ensures that other values set for the same option will prevail. ::: - ## Marker Styling: Color For better visual contrast, it would also be helpful to have a way to change the *color* of a marker. @@ -1060,6 +1058,7 @@ Make sure to also add a new line using this to the `attributes` list, so the opt ``` ## Wrapping Up + In this tutorial, you've learned how to write custom Nix modules to bring external services under declarative control, with the help of several new utility functions from the Nixpkgs `lib`. You defined several modules in multiple files, each with separate submodules making use of the module system's type checking. From cd12ffecb587399ee9557204b9149a8b151241aa Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 21:32:11 +0200 Subject: [PATCH 06/36] add some more motivation the each section --- source/tutorials/module-system/module-system.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 416f8dc031..a6f9cf582d 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -21,6 +21,7 @@ This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation ## Empty module +We have to start somewhere. The simplest module is just a function that takes any attributes and returns an empty attribute set. Write the following into a file called `default.nix`: @@ -49,7 +50,8 @@ Do this by adding the following new line to `default.nix`: The addition of this line turns the expression in `default.nix` into a function which takes *at least* one argument, called `lib`, and may accept other arguments (expressed by the ellipsis `...`). -Matching on the `lib` argument will make `nixpkgs` library functions available within the function body. +On NixOS, `lib` argument is passed automatically. +This will make Nixpkgs library functions available within the function body. :::{note} The ellipsis `...` is necessary because arbitrary arguments can be passed to modules. @@ -57,8 +59,10 @@ The ellipsis `...` is necessary because arbitrary arguments can be passed to mod ## Declaring Options -Modules allow providing *options* that declare which values can be set and used elsewhere. -Options are declared by defining an attribute under the top-level `options` attribute, using `lib.mkOption`. +To set any values, the module system first has to know which ones are allowed. + +This is done by declaring *options* that specify which values can be set and used elsewhere. +Options are declared by adding an attribute under the top-level `options` attribute, using `lib.mkOption`. In this section, you will define the `generate.script` option. @@ -102,6 +106,7 @@ nix-instantiate --eval eval.nix -A config.generate.script You will see an error message indicating that the `generate.script` option is used but not defined; you will need to assign a value to the option before using it. ## Type Checking + As previously mentioned, the `lines` type only permits string values. :::{warning} From d85f1425bf0cd71e7f14f2490935bfcfe5c8d975 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 21:56:45 +0200 Subject: [PATCH 07/36] make scripts downloadable --- source/conf.py | 2 +- source/tutorials/module-system/{ => files}/geocode | 0 source/tutorials/module-system/{ => files}/map | 0 source/tutorials/module-system/icat | 3 --- source/tutorials/module-system/module-system.md | 3 +++ 5 files changed, 4 insertions(+), 4 deletions(-) rename source/tutorials/module-system/{ => files}/geocode (100%) rename source/tutorials/module-system/{ => files}/map (100%) delete mode 100755 source/tutorials/module-system/icat diff --git a/source/conf.py b/source/conf.py index 76c0aaff40..6f347b610c 100644 --- a/source/conf.py +++ b/source/conf.py @@ -178,7 +178,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ['_static', 'tutorials/module-system/files'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied diff --git a/source/tutorials/module-system/geocode b/source/tutorials/module-system/files/geocode similarity index 100% rename from source/tutorials/module-system/geocode rename to source/tutorials/module-system/files/geocode diff --git a/source/tutorials/module-system/map b/source/tutorials/module-system/files/map similarity index 100% rename from source/tutorials/module-system/map rename to source/tutorials/module-system/files/map diff --git a/source/tutorials/module-system/icat b/source/tutorials/module-system/icat deleted file mode 100755 index cf71093132..0000000000 --- a/source/tutorials/module-system/icat +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -feh - diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index a6f9cf582d..4d4bf31e00 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -19,6 +19,9 @@ Be prepared to see some Nix errors: during the tutorial, you will first write so This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation on modules](https://infinisil.com/modules.mp4) [(source)](https://github.com/tweag/summer-of-nix-modules) for 2021 Summer of Nix. ::: +You will use need two helper scripts for this exercise. +Download {download}`map ` and {download}`geocode ` into your working directory. + ## Empty module We have to start somewhere. From d9b4b736ba9abadc73cdabe00fd686d968481ca4 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 21:57:14 +0200 Subject: [PATCH 08/36] address review comments --- .../tutorials/module-system/module-system.md | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 4d4bf31e00..d7247f9482 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -39,19 +39,19 @@ Write the following into a file called `default.nix`: ## Module Arguments -In order to make a module actually useful, you will need to write it as a *function*, which takes an attribute set as an argument. - -Do this by adding the following new line to `default.nix`: +We will need some helper functions, which will come from the Nixpkgs library. +Start by changing the first line in `default.nix`: ```diff # default.nix +- { ... }: + { lib, ... }: { } ``` -The addition of this line turns the expression in `default.nix` into a function which takes *at least* one argument, called `lib`, and may accept other arguments (expressed by the ellipsis `...`). +Now the module is a function which takes *at least* one argument, called `lib`, and may accept other arguments (expressed by the ellipsis `...`). On NixOS, `lib` argument is passed automatically. This will make Nixpkgs library functions available within the function body. @@ -85,21 +85,28 @@ Change `default.nix` to include the following declaration: ``` While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option. -There are several types available under [`lib.types`](https://github.com/NixOS/nixpkgs/blob/master/lib/types.nix) in the Nixpkgs library. +There are several types available under [`lib.types`](https://nixos.org/manual/nixos/stable/#sec-option-types-basic) in the Nixpkgs library. -You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple strings should be joined with newlines. +You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple definitions should be joined with newlines. Write a new file, `eval.nix`, which you will use to evaluate `default.nix`: ```nix # eval.nix -(import ).evalModules { +let + nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-22.11"; + pkgs = import nixpkgs { config = {}; overlays = []; }; +in +pkgs.lib.evalModules { modules = [ ./default.nix ]; } ``` +[`evalModules`] is the function that evaluates modules, applies type checking, and merges values into the final attribute set. +It expects a `modules` attribute that takes a list, where each element can be a path to a module or an expression that follows the [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules). + Now run the following command: ```bash From 40898395eec9cb89556e9e251ab8d6a7f18a819a Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 23:06:05 +0200 Subject: [PATCH 09/36] make script actually work ...hopefully. can't test it without Google API key --- .../tutorials/module-system/module-system.md | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index d7247f9482..dfe79d09d1 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -13,6 +13,10 @@ and follow extensive demonstration of how to wrap an existing API with Nix modul Concretely, you'll write modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. +:::{warning} +To run the examples in this tutorial, you will need a [Google API key](https://developers.google.com/maps/documentation/maps-static/start#before-you-begin) in `$XDG_DATA_HOME/google-api/key`. +::: + Be prepared to see some Nix errors: during the tutorial, you will first write some *incorrect* configurations, creating opportunities to discuss the resulting error messages and how to resolve them, particularly when discussing type checking. :::{note} @@ -168,15 +172,58 @@ Update `default.nix` by changing the value of `generate.script` to the following config = { - generate.script = 42; + generate.script = '' -+ map size=640x640 scale=2 | icat ++ ./map size=640x640 scale=2 | feh - + ''; }; ``` -TODO: Create derivations to get these commands +## Interlude: Reproducible scripts + +The simple script will likely not work as intended on your system, as it may lack the required dependencies. +We can solve this by packaging the raw {download}`map ` script with `pkgs.writeShellApplication`. + +First, make available a `pkgs` argument in your module evaluation by adding a module that sets `config._module.args`: + +```diff +# eval.nix + pkgs.lib.evalModules { + modules = [ ++ ({ config, ... }: { config._module.args = { inherit pkgs; }; }) + ./test.nix + ]; + } +``` + +Then change `default.nix` to have the following contents: + +```nix +# default.nix +{ pkgs, lib, ... }: { + + options = { + generate.script = lib.mkOption { + type = lib.types.package; + }; + }; + + config = { + generate.script = pkgs.writeShellApplication { + name = "map"; + runtimeInputs = with pkgs; [ curl feh ]; + text = '' + ${./map} size=640x640 scale=2 | feh - + ''; + }; + }; +} +``` + +This will access the previously added `pkgs` argument so we can use dependencies, and copy the `map` file in the current directory into the Nix store so it's available to the wrapped script, which will also live in the Nix store. ## Declaring More Options +Rather than setting all script parameters manually, we will to do that through the module system, as this will not just add some safety through type checking, but also allows to build abstractions in order to manage growing complexity and changing requirements. + In this section, you will introduce another option: `generate.requestParams`. For its type, you should use `listOf `, which is a list type where each element must have the specified type. @@ -189,10 +236,11 @@ Module option types not only check for valid values, but also specify how multip - For `str`, multiple definitions are not allowed. This is not a problem here, since one can't define a list element multiple times. Make the following additions to your `default.nix` file: + ```diff # default.nix generate.script = lib.mkOption { - type = lib.types.lines; + type = lib.types.package; }; + + generate.requestParams = lib.mkOption { @@ -200,17 +248,20 @@ Make the following additions to your `default.nix` file: + }; }; - config = { - generate.script = '' - map size=640x640 scale=2 | icat - ''; + config = { + generate.script = pkgs.writeShellApplication { + name = "map"; + runtimeInputs = with pkgs; [ curl feh ]; + text = '' + ${./map} size=640x640 scale=2 | feh - + ''; + }; + + generate.requestParams = [ + "size=640x640" + "scale=2" + ]; }; - } ``` @@ -225,13 +276,14 @@ Options can depend on other options, making it possible to build more useful abs Here, we want the `generate.script` option to use the values of `generate.requestParams` as arguments to the `map` command. ### Accessing Option Values + To make an option definition available, the argument of the module accessing it must include the `config` attribute. Update `default.nix` to add the `config` attribute: ```diff # default.nix --{ lib, ... }: { -+{ lib, config, ... }: { +-{ pkgs, lib, ... }: { ++{ pkgs, lib, config, ... }: { ``` When a module that sets options is evaluated, the resulting values can be accessed by their corresponding attribute names. @@ -307,6 +359,7 @@ Now make the following additions to the `generate.requestParams` list in the `co You have now declared options controlling the map dimensions and zoom level, but have not provided a way to specify where the map should be centered. Add the `center` option now, possibly with your own location as default value: + ```diff # default.nix type = lib.types.nullOr lib.types.int; From 003f0829fd6d749cf469b0108628cc07376714d2 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 23:41:44 +0200 Subject: [PATCH 10/36] add file watching yes, this looks scary, and yes, it works. --- .../tutorials/module-system/module-system.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index dfe79d09d1..3c266cf52f 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -220,6 +220,34 @@ Then change `default.nix` to have the following contents: This will access the previously added `pkgs` argument so we can use dependencies, and copy the `map` file in the current directory into the Nix store so it's available to the wrapped script, which will also live in the Nix store. +Run the script with: + +```console +nix-build eval.nix -A config.generate.script +./result/bin/map +``` + +To iterate more quickly, open a new terminal and set up [`entr`](https://github.com/eradman/entr) to re-run the script whenever any source file in the current directory changes: + +```console +nix-shell -p entr findutils bash --run \ + "ls *.nix | \ + entr -rs ' \ + nix-build eval.nix -A config.generate.script --no-out-link \ + | xargs printf -- \"%s/bin/map\" \ + | xargs bash \ + ' \ + " +``` + +This command does the following: +- List all `.nix` files +- Make `entr` watch them for changes. Terminate the invoked command on each change with `-r`. +- On each change: + - Run the `nix-build` invocation as above, but without adding a `./result` symlink + - Take the resulting store path and append `/bin/map` to it + - Run the executable at the path constructed this way + ## Declaring More Options Rather than setting all script parameters manually, we will to do that through the module system, as this will not just add some safety through type checking, but also allows to build abstractions in order to manage growing complexity and changing requirements. From 690752b87f898fbdd6d6bc3db73ac635bd287715 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 23:48:21 +0200 Subject: [PATCH 11/36] update diff and wording --- .../tutorials/module-system/module-system.md | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 3c266cf52f..5c0d6dcefa 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -250,7 +250,8 @@ This command does the following: ## Declaring More Options -Rather than setting all script parameters manually, we will to do that through the module system, as this will not just add some safety through type checking, but also allows to build abstractions in order to manage growing complexity and changing requirements. +Rather than setting all script parameters directly, we will to do that through the module system. +This will not just add some safety through type checking, but also allows to build abstractions in order to manage growing complexity and changing requirements. In this section, you will introduce another option: `generate.requestParams`. @@ -308,29 +309,37 @@ Here, we want the `generate.script` option to use the values of `generate.reques To make an option definition available, the argument of the module accessing it must include the `config` attribute. Update `default.nix` to add the `config` attribute: + ```diff # default.nix -{ pkgs, lib, ... }: { +{ pkgs, lib, config, ... }: { ``` -When a module that sets options is evaluated, the resulting values can be accessed by their corresponding attribute names. +When a module that sets options is evaluated, the resulting values can be accessed by their corresponding attribute names under `config`. Now make the following changes to `default.nix`: ```diff # default.nix + config = { - generate.script = '' -- map size=640x640 scale=2 | icat -+ map ${lib.concatStringsSep " " -+ config.generate.requestParams -+ } | icat - ''; + generate.script = pkgs.writeShellApplication { + name = "map"; + runtimeInputs = with pkgs; [ curl feh ]; + text = '' +- ${./map} size=640x640 scale=2 | feh - ++ map ${lib.concatStringsSep " " ++ config.generate.requestParams ++ } | feh - + ''; ``` Here, the value of the `config.generate.requestParams` attribute is populated by the module system based on the definitions in the same file. -Lazy evaluation in the Nix language allows taking a value from the `config` argument passed to the module which defines the value. + +:::{note} +Lazy evaluation in the Nix language allows the module system to make a value available in the `config` argument passed to the module which defines that value. +::: `lib.concatStringsSep " "` is then used to join each list element from the value of `config.generate.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. From b89a73733a0734f9f1fad4662943954d601ecba3 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Thu, 26 Oct 2023 23:59:53 +0200 Subject: [PATCH 12/36] more notes on potential pitfalls --- source/tutorials/module-system/module-system.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 5c0d6dcefa..cfde0a79d4 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -296,9 +296,7 @@ Make the following additions to your `default.nix` file: ## Dependencies Between Options -A given module generally only declares a single option that is meant to be evaluated. - -This option generates the final result to be used elsewhere, which in this case is `generate.script`. +A given module generally declares one option that produces a result to be used elsewhere, which in this case is `generate.script`. Options can depend on other options, making it possible to build more useful abstractions. @@ -318,6 +316,17 @@ Update `default.nix` to add the `config` attribute: When a module that sets options is evaluated, the resulting values can be accessed by their corresponding attribute names under `config`. +:::{note} +Option values can't be accessed directly from the same module. + +The module system evaluates all modules it receives, and any of them can define a particular option's value. +What happens when an option is set by multiple modules is determined by that option's type. + +The `config` argument is *not the same* as the `config` attribute where option values are set: +- The `config` argument holds the module system's evaluation result that takes into account all modules passed to `evalModules` and their `imports`. +- The `config` attribute of a module exposes that particular module's option values to the module system for evaluation. +::: + Now make the following changes to `default.nix`: ```diff From 202643acde2d741b1141ef0eb58dfe130e954da9 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 00:02:10 +0200 Subject: [PATCH 13/36] be explicit which `map` we mean --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index cfde0a79d4..51032e6aec 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -352,7 +352,7 @@ Lazy evaluation in the Nix language allows the module system to make a value ava `lib.concatStringsSep " "` is then used to join each list element from the value of `config.generate.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. -The result of this represents the list of command line arguments to pass to `map`. +The result of this represents the list of command line arguments to pass to the `map` script. ## Conditional Definitions and Default Values From dab738c9cacbfde70903bf24d23e5c633890938c Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 00:16:12 +0200 Subject: [PATCH 14/36] split nullable from default values --- .../tutorials/module-system/module-system.md | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 51032e6aec..8be99d61bc 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -304,7 +304,7 @@ Here, we want the `generate.script` option to use the values of `generate.reques ### Accessing Option Values -To make an option definition available, the argument of the module accessing it must include the `config` attribute. +To make option values available, the argument of the module accessing them must include the `config` attribute. Update `default.nix` to add the `config` attribute: @@ -354,19 +354,12 @@ Lazy evaluation in the Nix language allows the module system to make a value ava The result of this represents the list of command line arguments to pass to the `map` script. -## Conditional Definitions and Default Values +## Conditional Definitions In this section, you will define a new option, `map.zoom`, to control the zoom level of the map. -You will use a new type, `nullOr `, which can take either values of its argument type or `null`. - -In this case, a `null` value will use the API's default behavior of inferring the zoom level. - -Here, you will also use `default` from `mkOption`](https://github.com/NixOS/nixpkgs/blob/master/lib/options.nix) to declare your first *default* value, which will be used if the option declaring it is not enabled. - -You will use this option to define another element in `generate.requestParams`, which will only be added if its value is non-null. - -To do this, you can use the `mkIf ` function, which only adds the definition if the condition evaluates to `true`. +Since the Google Maps API will infer a zoom level if no corresponding argument is passed, we want to represent that at the module level. +To do that, you will use a new type, `nullOr `, which can take either values of its argument type or `null`. Add the `map` attribute set with the `zoom` option into the top-level `options` declaration, like so: @@ -379,18 +372,16 @@ Add the `map` attribute set with the `zoom` option into the top-level `options` + map = { + zoom = lib.mkOption { + type = lib.types.nullOr lib.types.int; -+ default = 2; + }; + }; }; - ``` +``` -Now make the following additions to the `generate.requestParams` list in the `config` block: +To make use of this, use the `mkIf ` function, which only adds the definition if the condition evaluates to `true`. +Make the following additions to the `generate.requestParams` list in the `config` block: ```diff # default.nix - config = { - ... generate.requestParams = [ "size=640x640" "scale=2" @@ -400,6 +391,28 @@ Now make the following additions to the `generate.requestParams` list in the `co }; ``` +This will will only add a `zoom` parameter to the script call if the value is non-null. + +## Default values + +Let's say that in our application we want to have a different default behavior that sets the the zoom level to `2`, such that automatic zoom has to be enabled explicitly. + +This can be done with the `default` argument to [`mkOption`](https://github.com/NixOS/nixpkgs/blob/master/lib/options.nix). +Its value will be used if the value of the option declaring it is otherwise specified. + +Add the corresponding line: + +```diff +# default.nix + map = { + zoom = lib.mkOption { + type = lib.types.nullOr lib.types.int; ++ default = 2; + }; + }; + }; +``` + ## Centering the Map You have now declared options controlling the map dimensions and zoom level, but have not provided a way to specify where the map should be centered. From 6cfbb37bf6be78c723fde04ffbb1feb534ff880a Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 00:58:41 +0200 Subject: [PATCH 15/36] also wrap the geocode script --- .../tutorials/module-system/module-system.md | 111 ++++++++++++------ 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 8be99d61bc..dab50f771b 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -433,9 +433,41 @@ Add the `center` option now, possibly with your own location as default value: }; ``` -To implement this behavior, you will use the `geocode` utility, which turns location names into coordinates. +To implement this behavior, you will use the {download}`geocode ` utility, which turns location names into coordinates. +There are multiple ways of making a new package accessible, but as an exercise, you will add it as an option in the module system. -Add another `mkIf` call to the list of `requestParams` now: +First, add a new option to accommodate the package: + + +```diff +# default.nix + options = { + generate.script = lib.mkOption { + type = lib.types.package; + }; ++ ++ helpers.geocode = lib.mkOption { ++ type = lib.types.package; ++ }; +``` + +Then define the value for that option where you make the raw script reproducible by wrapping a call to it in `writeShellApplication`: + +```diff +# default.nix + config = { ++ helpers.geocode = pkgs.writeShellApplication { ++ name = "geocode"; ++ runtimeInputs = with pkgs; [ curl jq ]; ++ text = "exec ${./geocode}"; ++ }; ++ + generate.script = pkgs.writeShellApplication { + name = "map"; + runtimeInputs = with pkgs; [ curl feh ]; +``` + +Add another `mkIf` call to the list of `requestParams` now where you access the wrapped package through `config.helpers.geocode`, and run the executable `/bin/geocode` inside: ```diff # default.nix @@ -443,16 +475,16 @@ Add another `mkIf` call to the list of `requestParams` now: (lib.mkIf (config.map.zoom != null) "zoom=${toString config.map.zoom}") + (lib.mkIf (config.map.center != null) -+ "center=\"$(geocode ${ ++ "center=\"$(${config.helpers.geocode}/bin/geocode ${ + lib.escapeShellArg config.map.center + })\"") ]; }; ``` -This time, you've used `escapeShellArg` to pass the `config.map.center` value as a command-line argument to `geocode`, interpolating the result back into the `requestParams` string which sets the `center` value. +This time, you've used `escapeShellArg` to pass the `config.map.center` value as a command-line argument to `geocode`, string interpolating the result back into the `requestParams` string which sets the `center` value. -Wrapping shell command execution in Nix modules is a helpful technique for controlling system changes, using the more ergonomic attributes and values interface rather than dealing with the peculiarities of escaping manually. +Wrapping shell command execution in Nix modules is a helpful technique for controlling system changes, as it uses the more ergonomic attributes and values interface rather than dealing with the peculiarities of escaping manually. ## Splitting Modules @@ -460,7 +492,8 @@ The [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules) In particular, this allows you to separate option declarations from where they are used in your configuration. -Create a new module, `marker.nix`, where you can declare options for defining location pins and other markers on the map. +Create a new module, `marker.nix`, where you can declare options for defining location pins and other markers on the map: + ```diff # marker.nix +{ lib, config, ... }: { @@ -469,9 +502,10 @@ Create a new module, `marker.nix`, where you can declare options for defining lo ``` Reference this new file in `default.nix` using the `imports` attribute: + ```diff # default.nix - { lib, config, ... }: { + { pkgs, lib, config ... }: { + imports = [ + ./marker.nix @@ -481,22 +515,23 @@ Reference this new file in `default.nix` using the `imports` attribute: ## The `submodule` Type -One of the most useful types included in the module system's type system is `submodule`. +We want to set multiple markers on the map. +A marker is a complex type with multiple fields. +This is wher one of the most useful types included in the module system's type system comes into play: `submodule`. This type allows you to define nested modules with their own options. - Here, you will define a new `map.markers` option whose type is a list of submodules, each with a nested `location` type, allowing you to define a list of markers on the map. Each assignment of markers will be type-checked during evaluation of the top-level `config`. -Make the following changes to `marker.nix` now: +Make the following changes to `marker.nix`: + ```diff # marker.nix --{ lib, config, ... }: { -+{ lib, config, ... }: +-{ pkgs, lib, config, ... }: { ++{ pkgs, lib, config, ... }: +let -+ + markerType = lib.types.submodule { + options = { + location = lib.mkOption { @@ -512,19 +547,18 @@ Make the following changes to `marker.nix` now: + type = lib.types.listOf markerType; + }; + }; - } ``` ## Setting Option Values Within Other Modules -Because of the way the module system composes option definitions, you can also freely assign values to options defined in other modules. +Because of the way the module system composes option definitions, you can freely assign values to options defined in other modules. -In this case, you will use the `map.markers` option to produce and add new elements to the `requestParams` list, making your declared markers appear on the returned map. +In this case, you will use the `map.markers` option to produce and add new elements to the `requestParams` list, making your declared markers appear on the returned map – but from the module declared in `marker.nix`. To implement this behavior, add the following `config` block to `marker.nix`: + ```diff # marker.nix - ... + config = { + + map.markers = [ @@ -544,20 +578,18 @@ To implement this behavior, add the following `config` block to `marker.nix`: + lib.concatStringsSep "\\|" attributes + }"; + in map paramForMarker config.map.markers; -+ -+ }; - } ``` Here, you again used `escapeShellArg` and string interpolation to generate a Nix string, this time producing a pipe-separated list of geocoded location attributes. -The `generate.requestParams` value was also set to the resulting list of strings, which gets appended to the `generate.requestParams` list defined in `default.nix`, thanks to the default merging behavior of the `list`-type module. +The `generate.requestParams` value was also set to the resulting list of strings, which gets appended to the `generate.requestParams` list defined in `default.nix`, thanks to the default merging behavior of the `list` type. + +## Dealing with multiple markers -## Multiple Markers +When defining multiple markers, determining an appropriate center or zoom level for the map may be challenging; it's easier to let the API do this for you. -In case you define multiple markers, determining an appropriate center or zoom level for the map may be challenging; it's easier to let the API do this for you. +To achieve this, make the following additions to `marker.nix`, above the `generate.requestParams` declaration: -To do this, make the following additions to `marker.nix`, above the `generate.requestParams` declaration: ```diff # marker.nix + map.center = lib.mkIf @@ -577,20 +609,20 @@ In this case, the default behavior of the Google Maps API when not passed a cent ## Nested Submodules -It's time to introduce a `users` option with type `lib.types.attrsOf `, which will allow you to define `users` as an attribute set, whose values have type ``. +Next, we want to allow multiple named users to define a list of markers each. + +For that you'll add a `users` option with type `lib.types.attrsOf `, which will allow you to define `users` as an attribute set, whose values have type ``. Here, that subtype will be another submodule which allows declaring a departure marker, suitable for querying the API for the recommended route for a trip. This will again make use of the `markerType` submodule, giving a nested structure of submodules. -To propagate marker definitions from `users` to the `map.markers` option, make the following changes now: +To propagate marker definitions from `users` to the `map.markers` option, make the following changes. -- In the `let` block: +In the `let` block: ```diff # marker.nix - let - ... + userType = lib.types.submodule { + options = { + departure = lib.mkOption { @@ -603,18 +635,21 @@ To propagate marker definitions from `users` to the `map.markers` option, make in { ``` -- In the `options` block, above `map.markers`: +This defines a submodule type for a user, with a `departure` option of type `markerType`. + +In the `options` block, above `map.markers`: + ```diff # marker.nix - options = { -+ + users = lib.mkOption { + type = lib.types.attrsOf userType; + }; -+ ``` -- In the `config` block, above `map.center`: +That allows adding a `users` attribute set to `config` in any submodule that imports `marker.nix`, where each attribute will be of type `userType` as declared in the previous step. + +In the `config` block, above `map.center`: + ```diff # marker.nix config = { @@ -632,13 +667,11 @@ To propagate marker definitions from `users` to the `map.markers` option, make (lib.length config.map.markers >= 1) ``` -The `config.users` attribute set is passed to `attrValues`, which returns a list of values of each of the attributes in the set (here, the set of `config.users` you've defined), sorted alphabetically. - -The `departure` values of each of the `users` are then joined into a list, and this list is filtered for non-`null` locations. +This takes all the `departure` markers from all users in the `config` argument, and adds them to `map.markers` if their `location` attribute is not `null`. -The resulting list is stored in `map.markers`. +The `config.users` attribute set is passed to `attrValues`, which returns a list of values of each of the attributes in the set (here, the set of `config.users` you've defined), sorted alphabetically (this how attribute names are stored in the Nix language). -The resulting `map.markers` option then propagates to the `generate.requestParams` option, which in turn is used to generate arguments to the script which ultimately calls the Google Maps API. +Back in `default.nix`, the resulting `map.markers` option value is still accessed by `generate.requestParams`, which in turn is used to generate arguments to the script that ultimately calls the Google Maps API. Defining the options in this way allows you to set multiple `users..departure.location` values and generate a map with the appropriate zoom and center, with pins corresponding to the set of `departure.location` values for *all* `users`. From 7d7f93997446c763e7738440232c28835c924b5b Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 01:38:58 +0200 Subject: [PATCH 16/36] work through the tutorial to the end --- .../tutorials/module-system/module-system.md | 173 ++++++++++-------- 1 file changed, 98 insertions(+), 75 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index dab50f771b..ac9beb85c6 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -338,7 +338,7 @@ Now make the following changes to `default.nix`: runtimeInputs = with pkgs; [ curl feh ]; text = '' - ${./map} size=640x640 scale=2 | feh - -+ map ${lib.concatStringsSep " " ++ builtins.map ${lib.concatStringsSep " " + config.generate.requestParams + } | feh - ''; @@ -496,9 +496,9 @@ Create a new module, `marker.nix`, where you can declare options for defining lo ```diff # marker.nix -+{ lib, config, ... }: { -+ -+} +{ lib, config, ... }: { + +} ``` Reference this new file in `default.nix` using the `imports` attribute: @@ -577,9 +577,14 @@ To implement this behavior, add the following `config` block to `marker.nix`: + in "markers=${ + lib.concatStringsSep "\\|" attributes + }"; -+ in map paramForMarker config.map.markers; ++ in builtins.map paramForMarker config.map.markers; ``` +:::{warning} +To avoid confusion with the `map` option setting and the evaluated `config.map` configuration value, here we use the `map` function explicitly as `builtins.map`. +::: + + Here, you again used `escapeShellArg` and string interpolation to generate a Nix string, this time producing a pipe-separated list of geocoded location attributes. The `generate.requestParams` value was also set to the resulting list of strings, which gets appended to the `generate.requestParams` list defined in `default.nix`, thanks to the default merging behavior of the `list` type. @@ -681,13 +686,14 @@ In the 2021 Summer of Nix, this formed the basis of an interactive multi-person Now that the map can be rendered with multiple markers, it's time to add some style customizations. -To tell the markers apart, you should add another option to the `markerType` submodule, to allow labeling each marker pin. +To tell the markers apart, add another option to the `markerType` submodule, to allow labeling each marker pin. The API documentation states that [these labels must be either an uppercase letter or a number](https://developers.google.com/maps/documentation/maps-static/start#MarkerStyles). -You can implement this with the `strMatching ""` type, where `` is a regular expression that will accept any matching (uppercase letter or number) values. +You can implement this with the `strMatching ""` type, where `` is a regular expression that will accept any matching values, in this case an uppercase letter or number. + +In the `let` block: -- In the `let` block: ```diff # marker.nix type = lib.types.nullOr lib.types.str; @@ -703,7 +709,10 @@ You can implement this with the `strMatching ""` type, where `` is }; ``` -- In the `paramForMarker` function: +Again, `types.nullOr` allows for `null` values, and the default has been set to `null`. + +In the `paramForMarker` function: + ```diff # marker.nix paramForMarker = marker: @@ -723,9 +732,8 @@ Here, the label for each `marker` is only propagated to the CLI parameters if `m ## Defining a Default Label -In case you don't want to manually define a label for each marker, you can set a default value. - -The easiest value for a default label is the username, which will always be set. +Right now, if a label is not explicitly set, none will show up. +But since every `users` attribute has a name, we could use that as an automatic value instead. This `firstUpperAlnum` function allows you to retrieve the first character of the username, with the correct type for passing to `departure.style.label`: @@ -746,9 +754,7 @@ This `firstUpperAlnum` function allows you to retrieve the first character of th By transforming the argument to `lib.types.submodule` into a function, you can access arguments within it. -One special argument available to submodules is the `name` argument, which when used in `attrsOf`, gives you the name of the attribute the submodule is defined under. - -You can use this function argument to retrieve the `name` attribute for use elsewhere: +One special argument automatically available to submodules is `name`, which when used in `attrsOf`, gives you the name of the attribute the submodule is defined under: ```diff # marker.nix @@ -781,23 +787,24 @@ Instead you can use the `config` section of the `user` submodule to set a defaul ``` :::{note} -Module options have a *precedence*, represented as an integer, which determines the priority of setting the option to a particular value. -When merging values, the lowest precedence wins. +Module options have a *priority*, represented as an integer, which determines the precedence for setting the option to a particular value. +When merging values, the priority with lowest numeric value wins. -The `lib.mkDefault` modifier sets the precedence of its argument value to 1000, the lowest priority. +The `lib.mkDefault` modifier sets the priority of its argument value to 1000, the lowest precedence. This ensures that other values set for the same option will prevail. ::: ## Marker Styling: Color -For better visual contrast, it would also be helpful to have a way to change the *color* of a marker. +For better visual contrast, it would be helpful to have a way to change the *color* of a marker. Here you will use two new type-functions for this: - `either `, which takes two types as arguments, and allows either of them - `enum [ ]`, which takes a list of allowed values, and allows any of them -In the `let` block, add the following `colorType` option, which can hold strings containing either some given color names or an RGB value: +In the `let` block, add the following `colorType` option, which can hold strings containing either some given color names or an RGB value add the new compound type: + ```diff # marker.nix ... @@ -816,7 +823,10 @@ In the `let` block, add the following `colorType` option, which can hold strings location = lib.mkOption { ``` -At the bottom of the `let` block, add the `style.color` option: +This allows either strings that matche a 24-bit hexadecimal number or are equal to one of the specified color names. + +At the bottom of the `let` block, add the `style.color` option and specify a default value: + ```diff # marker.nix (lib.types.strMatching "[A-Z0-9]"); @@ -831,7 +841,8 @@ At the bottom of the `let` block, add the `style.color` option: }; ``` -Now add a line to the `paramForMarker` list which makes use of the new option: +Now add an entry to the `paramForMarker` list which makes use of the new option: + ```diff # marker.nix (marker.style.label != null) @@ -847,7 +858,7 @@ Now add a line to the `paramForMarker` list which makes use of the new option: In case you set many different markers, it would be helpful to have the ability to change their size individually. -Add a new `style.size` option to `marker.nix`, allowing you to do so: +Add a new `style.size` option to `marker.nix`, allowing you to choose from the set of pre-defined sizes: ```diff # marker.nix @@ -865,6 +876,7 @@ Add a new `style.size` option to `marker.nix`, allowing you to do so: ``` Now add a mapping for the size parameter in `paramForMarker`, which selects an appropriate string to pass to the API: + ```diff # marker.nix generate.requestParams = let @@ -880,6 +892,7 @@ Now add a mapping for the size parameter in `paramForMarker`, which selects an a ``` Finally, add another `lib.optional` call to the `attributes` string, making use of the selected size: + ``` # marker.nix attributes = @@ -898,47 +911,50 @@ Finally, add another `lib.optional` call to the `attributes` string, making use So far, you've created an option for declaring a *destination* marker, as well as several options for configuring the marker's visual representation. +Now we want to compute and display a route from the user's location to some destination. + The new option defined in the next section will allow you to set an *arrival* marker, which together with a destination allows you to draw *paths* on the map using the new module defined below. To start, create a new `path.nix` file with the following contents: ```diff # path.nix -+{ lib, config, ... }: -+let -+ pathType = lib.types.submodule { -+ -+ options = { -+ locations = lib.mkOption { -+ type = lib.types.listOf lib.types.str; -+ }; -+ }; -+ -+ }; -+in { -+ options = { -+ map.paths = lib.mkOption { -+ type = lib.types.listOf pathType; -+ }; -+ }; -+ -+ config = { -+ generate.requestParams = let -+ attrForLocation = loc: -+ "$(geocode ${lib.escapeShellArg loc})"; -+ paramForPath = path: -+ let -+ attributes = -+ map attrForLocation path.locations; -+ in "path=${ -+ lib.concatStringsSep "\\|" attributes -+ }"; -+ in map paramForPath config.map.paths; -+ }; -+} +{ lib, config, ... }: +let + pathType = lib.types.submodule { + options = { + locations = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; + }; + }; +in { + options = { + map.paths = lib.mkOption { + type = lib.types.listOf pathType; + }; + }; + + config = { + generate.requestParams = let + attrForLocation = loc: + "$(geocode ${lib.escapeShellArg loc})"; + paramForPath = path: + let + attributes = + builtins.map attrForLocation path.locations; + in "path=${ + lib.concatStringsSep "\\|" attributes + }"; + in builtins.map paramForPath config.map.paths; + }; +} ``` -The `path.nix` module defines an option for declaring paths, augmenting the API call by re-using the `generate.requestParams` option. +The `path.nix` module defines an option for declaring a list of paths on our `map`, where each path is a list of strings for geographic locations. + + +In the `config` attribute we augment the API call by setting the `generate.requestParams` option value with the coordinates transformed appropriately, which will be concatenated with request paremeters set elsewhere. Now import this new `path.nix` module from your `marker.nix` module: @@ -957,7 +973,7 @@ Now import this new `path.nix` module from your `marker.nix` module: ## The Arrival Marker -Now copy the `departure` option declaration to a new `arrival` option in `marker.nix`, to complete the initial path implementation: +Copy the `departure` option declaration to a new `arrival` option in `marker.nix`, to complete the initial path implementation: ```diff # marker.nix @@ -973,6 +989,7 @@ Now copy the `departure` option declaration to a new `arrival` option in `marker ``` Next, add an `arrival.style.label` attribute to the `config` block, mirroring the `departure.style.label`: + ```diff # marker.nix config = { @@ -984,7 +1001,8 @@ Next, add an `arrival.style.label` attribute to the `config` block, mirroring th }); ``` -Finally, update the return list in the function passed to `concatMap` in `map.markers`: +Finally, update the return list in the function passed to `concatMap` in `map.markers` to also include the `arrival` marker for each user: + ```diff # marker.nix map.markers = lib.filter @@ -997,17 +1015,17 @@ Finally, update the return list in the function passed to `concatMap` in `map.ma map.center = lib.mkIf ``` -You should now be able to define paths on the map, connecting pairs of departure and arrival points. +Now you have the basesis to define paths on the map, connecting pairs of departure and arrival points. ## Connecting Markers by Paths -In the path module, you can now define a path connecting every user's departure and arrival locations. +In the path module, define a path connecting every user's departure and arrival locations: ```diff # path.nix config = { + -+ map.paths = map (user: { ++ map.paths = builtins.map (user: { + locations = [ + user.departure.location + user.arrival.location @@ -1022,10 +1040,6 @@ In the path module, you can now define a path connecting every user's departure "$(geocode ${lib.escapeShellArg loc})"; ``` -:::{warning} -Don't confuse the `map` function with the `map` option or the `map` script! -::: - The new `map.paths` attribute contains a list of all valid paths defined for all users. A path is valid only if the `departure` and `arrival` attributes are set for that user. @@ -1036,7 +1050,7 @@ Your users have spoken, and they demand the ability to customize the styles of t As before, you'll now declare a new submodule for the path style. -While you could also directly define the `style.weight` option, in this case, you should use the submodule in a future change to reuse the path style definitions. +While you could also directly declare the `style.weight` option, in this case you should use the submodule to be able reuse the path style type later. Add the `pathStyleType` submodule option to the `let` block in `path.nix`: ```diff @@ -1063,6 +1077,7 @@ The `ints.between ` type allows integers in the given (inclusive) The path weight will default to 5, but can be set to any integer value in the 1 to 20 range, with higher weights producing thicker paths on the map. Now add a `style` option to the `options` set further down the file: + ```diff # path.nix options = { @@ -1080,16 +1095,17 @@ Now add a `style` option to the `options` set further down the file: ``` Finally, update the `attributes` list in `paramForPath`: + ```diff # path.nix paramForPath = path: let attributes = -- map attrForLocation path.locations; +- builtins.map attrForLocation path.locations; + [ + "weight:${toString path.style.weight}" + ] -+ ++ map attrForLocation path.locations; ++ ++ builtins.map attrForLocation path.locations; in "path=${ lib.concatStringsSep "\\|" attributes }"; @@ -1097,11 +1113,12 @@ Finally, update the `attributes` list in `paramForPath`: ## The `pathStyle` Submodule -Users still can't actually customize the path style yet, so you should introduce a new `pathStyle` option for each user. +Users still can't actually customize the path style yet. +Introduce a new `pathStyle` option for each user. The module system allows you to declare values for an option multiple times, and if the types permit doing so, takes care of merging each declaration's values together. -This makes it possible to have a definition for the `user` option in the `marker.nix` module, as well as a `user` definition in `path.nix`, which you should add now: +This makes it possible to have a definition for the `user` option in the `marker.nix` module, as well as a `user` definition in `path.nix`: ```diff # path.nix @@ -1122,7 +1139,8 @@ This makes it possible to have a definition for the `user` option in the `marker }; ``` -Then add a line using the `user.pathStyle` option in `map.paths`: +Then add a line using the `user.pathStyle` option in `map.paths` where each user's paths are processed: + ```diff # path.nix user.departure.location @@ -1138,9 +1156,10 @@ Then add a line using the `user.pathStyle` option in `map.paths`: As with markers, paths should have customizable colors. -You can accomplish this using types you've already seen by now. +You can accomplish this using types you've already encountered by now. + +Add a new `colorType` block to `path.nix`, specifying the allowed color names and RGB/RGBA hexadecimal values: -Add a new `colorType` block to `path.nix`, specifying the allowed color names and RGB/RGBA values: ```diff # path.nix { lib, config, ... }: @@ -1158,6 +1177,7 @@ Add a new `colorType` block to `path.nix`, specifying the allowed color names an ``` Under the `weight` option, add a new `color` option to use the new `colorType` value: + ```diff # path.nix type = lib.types.ints.between 1 20; @@ -1173,6 +1193,7 @@ Under the `weight` option, add a new `color` option to use the new `colorType` v ``` Finally, add a line using the `color` option to the `attributes` list: + ```diff # path.nix attributes = @@ -1186,11 +1207,12 @@ Finally, add a line using the `color` option to the `attributes` list: ## Further Styling -Now that you've got this far, to further improve the aesthetics of the rendered map, you should add another style option allowing paths to be drawn as *geodesics*, the shortest "as the crow flies" distance between two points on Earth. +Now that you've got this far, to further improve the aesthetics of the rendered map, add another style option allowing paths to be drawn as *geodesics*, the shortest "as the crow flies" distance between two points on Earth. Since this feature can be turned on or off, you can do this using the `bool` type, which can be `true` or `false`. Make the following changes to `path.nix` now: + ```diff # path.nix type = colorType; @@ -1205,7 +1227,8 @@ Make the following changes to `path.nix` now: }; ``` -Make sure to also add a new line using this to the `attributes` list, so the option value is included in the API call: +Make sure to also add a line to use that value in `attributes` list, so the option value is included in the API call: + ```diff # path.nix [ From b9bef803232d4881230e345ee9ec65bcf360d799 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 02:08:36 +0200 Subject: [PATCH 17/36] add tutorial overview --- .../tutorials/module-system/module-system.md | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index ac9beb85c6..05e0953334 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -3,28 +3,44 @@ Much of the power in Nixpkgs and NixOS comes from the module system. It provides mechanisms for conveniently declaring and automatically merging interdependent attribute sets that follow dynamic type constraints, making it easy to express modular configurations. +## Overview + +This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation on modules](https://infinisil.com/modules.mp4) ([source](https://github.com/tweag/summer-of-nix-modules)) for participants of 2021 Summer of Nix. + +It may help playing it back alongside the tutorial to better keep track of changes to the code you will work on. + +### What will you learn? + In this tutorial you'll learn - what a module is - how to define one - what options are - how to declare them - how to express dependencies between modules + and follow extensive demonstration of how to wrap an existing API with Nix modules. Concretely, you'll write modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. +During the tutorial, you will first write some *incorrect* configurations, creating opportunities to discuss the resulting error messages and how to resolve them, particularly when discussing type checking. + +### What do you need? + +- Familiarity with data types and general programming +- A {ref}`Nix installation ` to run the examples +- Intermediate proficiency in reading and writing the Nix language + +You will use two helper scripts for this exercise. +Download {download}`map ` and {download}`geocode ` to your working directory. + :::{warning} To run the examples in this tutorial, you will need a [Google API key](https://developers.google.com/maps/documentation/maps-static/start#before-you-begin) in `$XDG_DATA_HOME/google-api/key`. ::: -Be prepared to see some Nix errors: during the tutorial, you will first write some *incorrect* configurations, creating opportunities to discuss the resulting error messages and how to resolve them, particularly when discussing type checking. - -:::{note} -This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation on modules](https://infinisil.com/modules.mp4) [(source)](https://github.com/tweag/summer-of-nix-modules) for 2021 Summer of Nix. -::: +### How long will it take? -You will use need two helper scripts for this exercise. -Download {download}`map ` and {download}`geocode ` into your working directory. +This is a very long tutorial. +Prepare for at least 3 hours of work. ## Empty module From 09f64ca35cc14c60ec6e4914d9ff085a7d9f776f Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 02:08:52 +0200 Subject: [PATCH 18/36] `lib` is always passed --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 05e0953334..ea9bbf9754 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -73,8 +73,8 @@ Start by changing the first line in `default.nix`: Now the module is a function which takes *at least* one argument, called `lib`, and may accept other arguments (expressed by the ellipsis `...`). -On NixOS, `lib` argument is passed automatically. This will make Nixpkgs library functions available within the function body. +The `lib` argument is passed automatically by the module system. :::{note} The ellipsis `...` is necessary because arbitrary arguments can be passed to modules. From ceb46bfab72ae2bece17a70ee6556270e02fb306 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 02:09:21 +0200 Subject: [PATCH 19/36] add separate section for `evalModules` and fix link --- source/tutorials/module-system/module-system.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index ea9bbf9754..d5977f0d04 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -109,6 +109,8 @@ There are several types available under [`lib.types`](https://nixos.org/manual/n You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple definitions should be joined with newlines. +## Evaluating modules + Write a new file, `eval.nix`, which you will use to evaluate `default.nix`: ```nix @@ -124,8 +126,8 @@ pkgs.lib.evalModules { } ``` -[`evalModules`] is the function that evaluates modules, applies type checking, and merges values into the final attribute set. -It expects a `modules` attribute that takes a list, where each element can be a path to a module or an expression that follows the [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules). +[`evalModules`](https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules) is the function that evaluates modules, applies type checking, and merges values into the final attribute set. +It expects a `modules` attribute that is a list, where each element can be a path to a module or an expression that follows the [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules). Now run the following command: From 3900543d1eb5238677c14199e79b6708659967bc Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 02:16:55 +0200 Subject: [PATCH 20/36] make option strucutre more self-explanatory --- .../tutorials/module-system/module-system.md | 99 ++++++++++--------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index d5977f0d04..4552cbc3ca 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -87,7 +87,7 @@ To set any values, the module system first has to know which ones are allowed. This is done by declaring *options* that specify which values can be set and used elsewhere. Options are declared by adding an attribute under the top-level `options` attribute, using `lib.mkOption`. -In this section, you will define the `generate.script` option. +In this section, you will define the `scripts.output` option. Change `default.nix` to include the following declaration: @@ -96,7 +96,7 @@ Change `default.nix` to include the following declaration: { lib, ... }: { + options = { -+ generate.script = lib.mkOption { ++ scripts.output = lib.mkOption { + type = lib.types.lines; + }; + }; @@ -107,7 +107,12 @@ Change `default.nix` to include the following declaration: While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option. There are several types available under [`lib.types`](https://nixos.org/manual/nixos/stable/#sec-option-types-basic) in the Nixpkgs library. -You have just declared `generate.script` with the `lines` type, which specifies that the only valid values are strings, and that multiple definitions should be joined with newlines. +You have just declared `scripts.output` with the `lines` type, which specifies that the only valid values are strings, and that multiple definitions should be joined with newlines. + +:::{note} +The name and attribute path of the option is arbitrary. +Here we use `scripts`, because we will add another script later, and call this one `output`, because it will output the resulting map. +::: ## Evaluating modules @@ -132,10 +137,10 @@ It expects a `modules` attribute that is a list, where each element can be a pat Now run the following command: ```bash -nix-instantiate --eval eval.nix -A config.generate.script +nix-instantiate --eval eval.nix -A config.scripts.output ``` -You will see an error message indicating that the `generate.script` option is used but not defined; you will need to assign a value to the option before using it. +You will see an error message indicating that the `scripts.output` option is used but not defined; you will need to assign a value to the option before using it. ## Type Checking @@ -154,13 +159,13 @@ Add the following lines to `default.nix`: { lib, ... }: { options = { - generate.script = lib.mkOption { + scripts.output = lib.mkOption { type = lib.types.lines; }; }; + config = { -+ generate.script = 42; ++ scripts.output = 42; + }; } ``` @@ -168,28 +173,28 @@ Add the following lines to `default.nix`: Now try to execute the previous command, and witness your first module error: ```console -$ nix-instantiate --eval eval.nix -A config.generate.script +$ nix-instantiate --eval eval.nix -A config.scripts.output error: ... - error: A definition for option `generate.script' is not of type `strings concatenated with "\n"'. Definition values: + error: A definition for option `scripts.output' is not of type `strings concatenated with "\n"'. Definition values: - In `/home/nix-user/default.nix': 42 ``` -This assignment of `generate.script = 42;` caused a type error: integers are not strings concatenated with the newline character. +This assignment of `scripts.output = 42;` caused a type error: integers are not strings concatenated with the newline character. ## Successful Type-checking -To make this module pass the type-checker and successfully evaluate the `generate.script` option, you will now assign a string to `generate.script`. +To make this module pass the type-checker and successfully evaluate the `scripts.output` option, you will now assign a string to `scripts.output`. In this case, you will assign a `map` script which first calls the Google Maps Static API to generate a world map, then displays the result using `icat` (image-cat), both of which are helper scripts. -Update `default.nix` by changing the value of `generate.script` to the following string: +Update `default.nix` by changing the value of `scripts.output` to the following string: ```diff # default.nix config = { -- generate.script = 42; -+ generate.script = '' +- scripts.output = 42; ++ scripts.output = '' + ./map size=640x640 scale=2 | feh - + ''; }; @@ -219,13 +224,13 @@ Then change `default.nix` to have the following contents: { pkgs, lib, ... }: { options = { - generate.script = lib.mkOption { + scripts.output = lib.mkOption { type = lib.types.package; }; }; config = { - generate.script = pkgs.writeShellApplication { + scripts.output = pkgs.writeShellApplication { name = "map"; runtimeInputs = with pkgs; [ curl feh ]; text = '' @@ -241,7 +246,7 @@ This will access the previously added `pkgs` argument so we can use dependencies Run the script with: ```console -nix-build eval.nix -A config.generate.script +nix-build eval.nix -A config.scripts.output ./result/bin/map ``` @@ -251,7 +256,7 @@ To iterate more quickly, open a new terminal and set up [`entr`](https://github. nix-shell -p entr findutils bash --run \ "ls *.nix | \ entr -rs ' \ - nix-build eval.nix -A config.generate.script --no-out-link \ + nix-build eval.nix -A config.scripts.output --no-out-link \ | xargs printf -- \"%s/bin/map\" \ | xargs bash \ ' \ @@ -271,7 +276,7 @@ This command does the following: Rather than setting all script parameters directly, we will to do that through the module system. This will not just add some safety through type checking, but also allows to build abstractions in order to manage growing complexity and changing requirements. -In this section, you will introduce another option: `generate.requestParams`. +In this section, you will introduce another option: `requestParams`. For its type, you should use `listOf `, which is a list type where each element must have the specified type. @@ -286,17 +291,17 @@ Make the following additions to your `default.nix` file: ```diff # default.nix - generate.script = lib.mkOption { + scripts.output = lib.mkOption { type = lib.types.package; }; + -+ generate.requestParams = lib.mkOption { ++ requestParams = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; }; config = { - generate.script = pkgs.writeShellApplication { + scripts.output = pkgs.writeShellApplication { name = "map"; runtimeInputs = with pkgs; [ curl feh ]; text = '' @@ -304,7 +309,7 @@ Make the following additions to your `default.nix` file: ''; }; + -+ generate.requestParams = [ ++ requestParams = [ + "size=640x640" + "scale=2" + ]; @@ -314,11 +319,11 @@ Make the following additions to your `default.nix` file: ## Dependencies Between Options -A given module generally declares one option that produces a result to be used elsewhere, which in this case is `generate.script`. +A given module generally declares one option that produces a result to be used elsewhere, which in this case is `scripts.output`. Options can depend on other options, making it possible to build more useful abstractions. -Here, we want the `generate.script` option to use the values of `generate.requestParams` as arguments to the `map` command. +Here, we want the `scripts.output` option to use the values of `requestParams` as arguments to the `map` command. ### Accessing Option Values @@ -351,24 +356,24 @@ Now make the following changes to `default.nix`: # default.nix config = { - generate.script = pkgs.writeShellApplication { + scripts.output = pkgs.writeShellApplication { name = "map"; runtimeInputs = with pkgs; [ curl feh ]; text = '' - ${./map} size=640x640 scale=2 | feh - + builtins.map ${lib.concatStringsSep " " -+ config.generate.requestParams ++ config.requestParams + } | feh - ''; ``` -Here, the value of the `config.generate.requestParams` attribute is populated by the module system based on the definitions in the same file. +Here, the value of the `config.requestParams` attribute is populated by the module system based on the definitions in the same file. :::{note} Lazy evaluation in the Nix language allows the module system to make a value available in the `config` argument passed to the module which defines that value. ::: -`lib.concatStringsSep " "` is then used to join each list element from the value of `config.generate.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. +`lib.concatStringsSep " "` is then used to join each list element from the value of `config.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. The result of this represents the list of command line arguments to pass to the `map` script. @@ -383,7 +388,7 @@ Add the `map` attribute set with the `zoom` option into the top-level `options` ```diff # default.nix - generate.requestParams = lib.mkOption { + requestParams = lib.mkOption { type = lib.types.listOf lib.types.str; }; + @@ -396,11 +401,11 @@ Add the `map` attribute set with the `zoom` option into the top-level `options` ``` To make use of this, use the `mkIf ` function, which only adds the definition if the condition evaluates to `true`. -Make the following additions to the `generate.requestParams` list in the `config` block: +Make the following additions to the `requestParams` list in the `config` block: ```diff # default.nix - generate.requestParams = [ + requestParams = [ "size=640x640" "scale=2" + (lib.mkIf (config.map.zoom != null) @@ -460,11 +465,11 @@ First, add a new option to accommodate the package: ```diff # default.nix options = { - generate.script = lib.mkOption { + scripts.output = lib.mkOption { type = lib.types.package; }; + -+ helpers.geocode = lib.mkOption { ++ scripts.geocode = lib.mkOption { + type = lib.types.package; + }; ``` @@ -474,18 +479,18 @@ Then define the value for that option where you make the raw script reproducible ```diff # default.nix config = { -+ helpers.geocode = pkgs.writeShellApplication { ++ scripts.geocode = pkgs.writeShellApplication { + name = "geocode"; + runtimeInputs = with pkgs; [ curl jq ]; + text = "exec ${./geocode}"; + }; + - generate.script = pkgs.writeShellApplication { + scripts.output = pkgs.writeShellApplication { name = "map"; runtimeInputs = with pkgs; [ curl feh ]; ``` -Add another `mkIf` call to the list of `requestParams` now where you access the wrapped package through `config.helpers.geocode`, and run the executable `/bin/geocode` inside: +Add another `mkIf` call to the list of `requestParams` now where you access the wrapped package through `config.scripts.geocode`, and run the executable `/bin/geocode` inside: ```diff # default.nix @@ -493,7 +498,7 @@ Add another `mkIf` call to the list of `requestParams` now where you access the (lib.mkIf (config.map.zoom != null) "zoom=${toString config.map.zoom}") + (lib.mkIf (config.map.center != null) -+ "center=\"$(${config.helpers.geocode}/bin/geocode ${ ++ "center=\"$(${config.scripts.geocode}/bin/geocode ${ + lib.escapeShellArg config.map.center + })\"") ]; @@ -583,7 +588,7 @@ To implement this behavior, add the following `config` block to `marker.nix`: + { location = "new york"; } + ]; + -+ generate.requestParams = let ++ requestParams = let + paramForMarker = marker: + let + attributes = @@ -605,13 +610,13 @@ To avoid confusion with the `map` option setting and the evaluated `config.map` Here, you again used `escapeShellArg` and string interpolation to generate a Nix string, this time producing a pipe-separated list of geocoded location attributes. -The `generate.requestParams` value was also set to the resulting list of strings, which gets appended to the `generate.requestParams` list defined in `default.nix`, thanks to the default merging behavior of the `list` type. +The `requestParams` value was also set to the resulting list of strings, which gets appended to the `requestParams` list defined in `default.nix`, thanks to the default merging behavior of the `list` type. ## Dealing with multiple markers When defining multiple markers, determining an appropriate center or zoom level for the map may be challenging; it's easier to let the API do this for you. -To achieve this, make the following additions to `marker.nix`, above the `generate.requestParams` declaration: +To achieve this, make the following additions to `marker.nix`, above the `requestParams` declaration: ```diff # marker.nix @@ -623,7 +628,7 @@ To achieve this, make the following additions to `marker.nix`, above the `genera + (lib.length config.map.markers >= 2) + null; + - generate.requestParams = let + requestParams = let paramForMarker = marker: let ``` @@ -694,7 +699,7 @@ This takes all the `departure` markers from all users in the `config` argument, The `config.users` attribute set is passed to `attrValues`, which returns a list of values of each of the attributes in the set (here, the set of `config.users` you've defined), sorted alphabetically (this how attribute names are stored in the Nix language). -Back in `default.nix`, the resulting `map.markers` option value is still accessed by `generate.requestParams`, which in turn is used to generate arguments to the script that ultimately calls the Google Maps API. +Back in `default.nix`, the resulting `map.markers` option value is still accessed by `requestParams`, which in turn is used to generate arguments to the script that ultimately calls the Google Maps API. Defining the options in this way allows you to set multiple `users..departure.location` values and generate a map with the appropriate zoom and center, with pins corresponding to the set of `departure.location` values for *all* `users`. @@ -897,7 +902,7 @@ Now add a mapping for the size parameter in `paramForMarker`, which selects an a ```diff # marker.nix - generate.requestParams = let + requestParams = let paramForMarker = marker: let + size = { @@ -954,7 +959,7 @@ in { }; config = { - generate.requestParams = let + requestParams = let attrForLocation = loc: "$(geocode ${lib.escapeShellArg loc})"; paramForPath = path: @@ -972,7 +977,7 @@ in { The `path.nix` module defines an option for declaring a list of paths on our `map`, where each path is a list of strings for geographic locations. -In the `config` attribute we augment the API call by setting the `generate.requestParams` option value with the coordinates transformed appropriately, which will be concatenated with request paremeters set elsewhere. +In the `config` attribute we augment the API call by setting the `requestParams` option value with the coordinates transformed appropriately, which will be concatenated with request paremeters set elsewhere. Now import this new `path.nix` module from your `marker.nix` module: @@ -1053,7 +1058,7 @@ In the path module, define a path connecting every user's departure and arrival + && user.arrival.location != null + ) (lib.attrValues config.users)); + - generate.requestParams = let + requestParams = let attrForLocation = loc: "$(geocode ${lib.escapeShellArg loc})"; ``` From 5929af291bc43ec3240c88e7dee1917e724887d4 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 02:30:57 +0200 Subject: [PATCH 21/36] explain command line invocations --- .../tutorials/module-system/module-system.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 4552cbc3ca..79fc9ddae9 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -134,13 +134,22 @@ pkgs.lib.evalModules { [`evalModules`](https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules) is the function that evaluates modules, applies type checking, and merges values into the final attribute set. It expects a `modules` attribute that is a list, where each element can be a path to a module or an expression that follows the [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules). -Now run the following command: +Run the following command: + +:::{warning} +This will result in an error. +::: ```bash nix-instantiate --eval eval.nix -A config.scripts.output ``` -You will see an error message indicating that the `scripts.output` option is used but not defined; you will need to assign a value to the option before using it. +[`nix-instantiate --eval`](https://nixos.org/manual/nix/stable/command-ref/nix-instantiate) parses and evaluates the Nix file at the specified path, and prints the result. +`evalModules` produces an attribute set where the final configuration values appear in the `config` attribute. +Therefore we evaluate the Nix expression in `eval.nix` at the [attribute path](https://nixos.org/manual/nix/stable/language/operators#attribute-selection) `config.scripts.output`. + +The error message indicates that the `scripts.output` option is used but not defined: a value must be set for the option before accessing it. +You will do this in the next steps. ## Type Checking @@ -186,7 +195,9 @@ This assignment of `scripts.output = 42;` caused a type error: integers are not To make this module pass the type-checker and successfully evaluate the `scripts.output` option, you will now assign a string to `scripts.output`. -In this case, you will assign a `map` script which first calls the Google Maps Static API to generate a world map, then displays the result using `icat` (image-cat), both of which are helper scripts. +In this case, you will assign a shell command that runs the {download}`map ` script in the current directory. +That in turn calls the Google Maps Static API to generate a world map. +The output is passed on to display it with [`feh`](https://feh.finalrewind.org/), a minimalistic image viewer. Update `default.nix` by changing the value of `scripts.output` to the following string: @@ -202,7 +213,7 @@ Update `default.nix` by changing the value of `scripts.output` to the following ## Interlude: Reproducible scripts -The simple script will likely not work as intended on your system, as it may lack the required dependencies. +That simple command will likely not work as intended on your system, as the script may lack the required dependencies. We can solve this by packaging the raw {download}`map ` script with `pkgs.writeShellApplication`. First, make available a `pkgs` argument in your module evaluation by adding a module that sets `config._module.args`: From b80fdbe87fbf2877507f195b9c25f1d204700400 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 02:48:38 +0200 Subject: [PATCH 22/36] add note on incomplete reference documentation --- source/tutorials/module-system/module-system.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 79fc9ddae9..27213164c5 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -228,6 +228,10 @@ First, make available a `pkgs` argument in your module evaluation by adding a mo } ``` +:::{note} +This mechanism is currently only [documented in the module system code](https://github.com/NixOS/nixpkgs/blob/master/lib/modules.nix#L140-L182), and that documentation is incomplete and out of date. +::: + Then change `default.nix` to have the following contents: ```nix From b65bb585f917ea28d295ea548ab576ba09a09a08 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 02:53:18 +0200 Subject: [PATCH 23/36] add more highlight to the `config` distinction --- source/tutorials/module-system/module-system.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 27213164c5..ae47575840 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -359,7 +359,9 @@ Option values can't be accessed directly from the same module. The module system evaluates all modules it receives, and any of them can define a particular option's value. What happens when an option is set by multiple modules is determined by that option's type. +::: +:::{warning} The `config` argument is *not the same* as the `config` attribute where option values are set: - The `config` argument holds the module system's evaluation result that takes into account all modules passed to `evalModules` and their `imports`. - The `config` attribute of a module exposes that particular module's option values to the module system for evaluation. From 3022f0b75810b4fb0fc1a5427766b04072c9413b Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 03:00:42 +0200 Subject: [PATCH 24/36] fix parameter passing to the `./map` script --- source/tutorials/module-system/module-system.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index ae47575840..f3ef5c5437 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -338,7 +338,7 @@ A given module generally declares one option that produces a result to be used e Options can depend on other options, making it possible to build more useful abstractions. -Here, we want the `scripts.output` option to use the values of `requestParams` as arguments to the `map` command. +Here, we want the `scripts.output` option to use the values of `requestParams` as arguments to the `./map` script. ### Accessing Option Values @@ -371,16 +371,14 @@ Now make the following changes to `default.nix`: ```diff # default.nix - config = { scripts.output = pkgs.writeShellApplication { name = "map"; runtimeInputs = with pkgs; [ curl feh ]; text = '' - ${./map} size=640x640 scale=2 | feh - -+ builtins.map ${lib.concatStringsSep " " -+ config.requestParams -+ } | feh - ++ ${./map} ${lib.concatStringsSep " " ++ config.requestParams} | feh - ''; ``` @@ -392,7 +390,7 @@ Lazy evaluation in the Nix language allows the module system to make a value ava `lib.concatStringsSep " "` is then used to join each list element from the value of `config.requestParams` into a single string, with the list elements of `requestParams` separated by a space character. -The result of this represents the list of command line arguments to pass to the `map` script. +The result of this represents the list of command line arguments to pass to the `./map` script. ## Conditional Definitions From c8c8b9aaf792ca2c15bf635b1c115ca941583b26 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 03:08:27 +0200 Subject: [PATCH 25/36] fix typo --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index f3ef5c5437..180f125373 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -436,7 +436,7 @@ This will will only add a `zoom` parameter to the script call if the value is no Let's say that in our application we want to have a different default behavior that sets the the zoom level to `2`, such that automatic zoom has to be enabled explicitly. This can be done with the `default` argument to [`mkOption`](https://github.com/NixOS/nixpkgs/blob/master/lib/options.nix). -Its value will be used if the value of the option declaring it is otherwise specified. +Its value will be used if the value of the option declaring it is not specified otherwise. Add the corresponding line: From 110fa17a7ea10369ebe34cb1d377a9e16bbf3558 Mon Sep 17 00:00:00 2001 From: fricklerhandwerk Date: Fri, 27 Oct 2023 03:10:19 +0200 Subject: [PATCH 26/36] fix wording --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 180f125373..5092735771 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -619,7 +619,7 @@ To implement this behavior, add the following `config` block to `marker.nix`: ``` :::{warning} -To avoid confusion with the `map` option setting and the evaluated `config.map` configuration value, here we use the `map` function explicitly as `builtins.map`. +To avoid confusion with the `map` option setting and the final `config.map` configuration value, here we use the `map` function explicitly as `builtins.map`. ::: From 0231c029992c3042d1f35b649db20a52263238e6 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 27 Oct 2023 15:45:54 +0200 Subject: [PATCH 27/36] link to summer of nix Co-authored-by: asymmetric --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 5092735771..979500622a 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -5,7 +5,7 @@ It provides mechanisms for conveniently declaring and automatically merging inte ## Overview -This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation on modules](https://infinisil.com/modules.mp4) ([source](https://github.com/tweag/summer-of-nix-modules)) for participants of 2021 Summer of Nix. +This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation on modules](https://infinisil.com/modules.mp4) ([source](https://github.com/tweag/summer-of-nix-modules)) for participants of [Summer of Nix](https://github.com/ngi-nix/summer-of-nix) 2021. It may help playing it back alongside the tutorial to better keep track of changes to the code you will work on. From 38e6d7d62fb98ecec625945bef422656883edbcc Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 27 Oct 2023 15:46:45 +0200 Subject: [PATCH 28/36] add missing word Co-authored-by: asymmetric --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 979500622a..3fbced80a9 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -18,7 +18,7 @@ In this tutorial you'll learn - how to declare them - how to express dependencies between modules -and follow extensive demonstration of how to wrap an existing API with Nix modules. +and follow an extensive demonstration of how to wrap an existing API with Nix modules. Concretely, you'll write modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. From 41b8eedef8c279bdfcc61362c240bbbd8ddec9d0 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 27 Oct 2023 15:47:04 +0200 Subject: [PATCH 29/36] link to Google Maps API docs Co-authored-by: asymmetric --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 3fbced80a9..5afaf23986 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -20,7 +20,7 @@ In this tutorial you'll learn and follow an extensive demonstration of how to wrap an existing API with Nix modules. -Concretely, you'll write modules to interact with the Google Maps API, declaring options which represent map geometry, location pins, and more. +Concretely, you'll write modules to interact with the [Google Maps API](https://developers.google.com/maps/documentation/maps-static), declaring options which represent map geometry, location pins, and more. During the tutorial, you will first write some *incorrect* configurations, creating opportunities to discuss the resulting error messages and how to resolve them, particularly when discussing type checking. From 4e054c36ad7db94411122c21385cb869913ae3f6 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 27 Oct 2023 15:47:39 +0200 Subject: [PATCH 30/36] more explicit requirements Co-authored-by: asymmetric --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 5afaf23986..d7874796ab 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -26,7 +26,7 @@ During the tutorial, you will first write some *incorrect* configurations, creat ### What do you need? -- Familiarity with data types and general programming +- Familiarity with data types and general programming concepts - A {ref}`Nix installation ` to run the examples - Intermediate proficiency in reading and writing the Nix language From bb20f343934c83729750062991886e310a54d63d Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 27 Oct 2023 15:53:27 +0200 Subject: [PATCH 31/36] use correct module system terminology Co-authored-by: asymmetric --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index d7874796ab..f53727f368 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -989,7 +989,7 @@ in { } ``` -The `path.nix` module defines an option for declaring a list of paths on our `map`, where each path is a list of strings for geographic locations. +The `path.nix` module declares an option for defining a list of paths on our `map`, where each path is a list of strings for geographic locations. In the `config` attribute we augment the API call by setting the `requestParams` option value with the coordinates transformed appropriately, which will be concatenated with request paremeters set elsewhere. From b8a36c37baf25d3f2d57251876a413078165367b Mon Sep 17 00:00:00 2001 From: asymmetric Date: Fri, 3 Nov 2023 09:04:48 +0000 Subject: [PATCH 32/36] Update source/tutorials/module-system/module-system.md --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index f53727f368..2e29b2e8ce 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -7,7 +7,7 @@ It provides mechanisms for conveniently declaring and automatically merging inte This tutorial follows [@infinisil](https://github.com/infinisil)'s [presentation on modules](https://infinisil.com/modules.mp4) ([source](https://github.com/tweag/summer-of-nix-modules)) for participants of [Summer of Nix](https://github.com/ngi-nix/summer-of-nix) 2021. -It may help playing it back alongside the tutorial to better keep track of changes to the code you will work on. +It may help playing it alongside this tutorial to better keep track of changes to the code you will work on. ### What will you learn? From c775e750d4d17194dcbc71c9885e8e189c1d1494 Mon Sep 17 00:00:00 2001 From: asymmetric Date: Fri, 3 Nov 2023 11:58:52 +0000 Subject: [PATCH 33/36] Apply suggestions from code review --- .../tutorials/module-system/module-system.md | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 2e29b2e8ce..692ad9921f 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -132,7 +132,7 @@ pkgs.lib.evalModules { ``` [`evalModules`](https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules) is the function that evaluates modules, applies type checking, and merges values into the final attribute set. -It expects a `modules` attribute that is a list, where each element can be a path to a module or an expression that follows the [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules). +It expects a `modules` attribute whose value is a list, where each element can be a path to a module or an expression that follows the [module schema](https://nixos.org/manual/nixos/stable/#sec-writing-modules). Run the following command: @@ -140,7 +140,7 @@ Run the following command: This will result in an error. ::: -```bash +```console nix-instantiate --eval eval.nix -A config.scripts.output ``` @@ -189,11 +189,11 @@ error: - In `/home/nix-user/default.nix': 42 ``` -This assignment of `scripts.output = 42;` caused a type error: integers are not strings concatenated with the newline character. +The definition `scripts.output = 42;` caused a type error: integers are not strings concatenated with the newline character. ## Successful Type-checking -To make this module pass the type-checker and successfully evaluate the `scripts.output` option, you will now assign a string to `scripts.output`. +To make this module pass the type checks and successfully evaluate the `scripts.output` option, you will now assign a string to `scripts.output`. In this case, you will assign a shell command that runs the {download}`map ` script in the current directory. That in turn calls the Google Maps Static API to generate a world map. @@ -213,7 +213,7 @@ Update `default.nix` by changing the value of `scripts.output` to the following ## Interlude: Reproducible scripts -That simple command will likely not work as intended on your system, as the script may lack the required dependencies. +That simple command will likely not work as intended on your system, as it may lack the required dependencies (curl and feh). We can solve this by packaging the raw {download}`map ` script with `pkgs.writeShellApplication`. First, make available a `pkgs` argument in your module evaluation by adding a module that sets `config._module.args`: @@ -289,11 +289,11 @@ This command does the following: ## Declaring More Options Rather than setting all script parameters directly, we will to do that through the module system. -This will not just add some safety through type checking, but also allows to build abstractions in order to manage growing complexity and changing requirements. +This will not just add some safety through type checking, but also allow to build abstractions to manage growing complexity and changing requirements. -In this section, you will introduce another option: `requestParams`. +Let's begin by introducing another option, `requestParams`, which will represent the parameters of the request made to the Google Maps API. -For its type, you should use `listOf `, which is a list type where each element must have the specified type. +Its type will be `listOf `, which is a list of elements of one type. Instead of `lines`, in this case you will want the type of the list elements to be `str`, a generic string type. @@ -334,7 +334,7 @@ Make the following additions to your `default.nix` file: ## Dependencies Between Options -A given module generally declares one option that produces a result to be used elsewhere, which in this case is `scripts.output`. +A given module generally declares one option that produces a result to be used elsewhere, in this case `scripts.output`. Options can depend on other options, making it possible to build more useful abstractions. @@ -342,7 +342,7 @@ Here, we want the `scripts.output` option to use the values of `requestParams` a ### Accessing Option Values -To make option values available, the argument of the module accessing them must include the `config` attribute. +To make option values available to a module, the arguments of the function declaring the module must include the `config` attribute. Update `default.nix` to add the `config` attribute: @@ -362,9 +362,9 @@ What happens when an option is set by multiple modules is determined by that opt ::: :::{warning} -The `config` argument is *not the same* as the `config` attribute where option values are set: -- The `config` argument holds the module system's evaluation result that takes into account all modules passed to `evalModules` and their `imports`. -- The `config` attribute of a module exposes that particular module's option values to the module system for evaluation. +The `config` *argument* is **not** the same as the `config` *attribute*: +- The `config` *argument* holds the result of the module system's lazy evaluation, which takes into account all modules passed to `evalModules` and their `imports`. +- The `config` *attribute* of a module exposes that particular module's option values to the module system for evaluation. ::: Now make the following changes to `default.nix`: @@ -393,11 +393,9 @@ Lazy evaluation in the Nix language allows the module system to make a value ava The result of this represents the list of command line arguments to pass to the `./map` script. ## Conditional Definitions +Sometimes, you will want option values to be, well, optional. This can be useful when defining a value for an option is not required, as in the following case. -In this section, you will define a new option, `map.zoom`, to control the zoom level of the map. - -Since the Google Maps API will infer a zoom level if no corresponding argument is passed, we want to represent that at the module level. -To do that, you will use a new type, `nullOr `, which can take either values of its argument type or `null`. +You will define a new option, `map.zoom`, to control the zoom level of the map. The Google Maps API will infer a zoom level if no corresponding argument is passed, a situation you can represent with the `nullOr `, which represents values of type `` or `null`. This means that when the option isn't defined, the value of such an option is `null`, a value that can be checked against in a conditional. Add the `map` attribute set with the `zoom` option into the top-level `options` declaration, like so: @@ -429,11 +427,11 @@ Make the following additions to the `requestParams` list in the `config` block: }; ``` -This will will only add a `zoom` parameter to the script call if the value is non-null. +This will will only add a `zoom` parameter to the script invocation if the value of `config.map.zoom` is not `null`. ## Default values -Let's say that in our application we want to have a different default behavior that sets the the zoom level to `2`, such that automatic zoom has to be enabled explicitly. +Let's say that in our application we want to have a different default behavior that sets the zoom level to `2`, such that automatic zooming has to be enabled explicitly. This can be done with the `default` argument to [`mkOption`](https://github.com/NixOS/nixpkgs/blob/master/lib/options.nix). Its value will be used if the value of the option declaring it is not specified otherwise. @@ -712,7 +710,7 @@ In the `config` block, above `map.center`: This takes all the `departure` markers from all users in the `config` argument, and adds them to `map.markers` if their `location` attribute is not `null`. -The `config.users` attribute set is passed to `attrValues`, which returns a list of values of each of the attributes in the set (here, the set of `config.users` you've defined), sorted alphabetically (this how attribute names are stored in the Nix language). +The `config.users` attribute set is passed to `attrValues`, which returns a list of values of each of the attributes in the set (here, the set of `config.users` you've defined), sorted alphabetically (which is how attribute names are stored in the Nix language). Back in `default.nix`, the resulting `map.markers` option value is still accessed by `requestParams`, which in turn is used to generate arguments to the script that ultimately calls the Google Maps API. @@ -955,7 +953,7 @@ The new option defined in the next section will allow you to set an *arrival* ma To start, create a new `path.nix` file with the following contents: -```diff +```nix # path.nix { lib, config, ... }: let From a25b5495a90ed96734e44947a43815d4d8549869 Mon Sep 17 00:00:00 2001 From: Lorenzo Manacorda Date: Fri, 3 Nov 2023 13:01:34 +0100 Subject: [PATCH 34/36] whitespace --- source/tutorials/module-system/module-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 692ad9921f..47f776353c 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -393,7 +393,7 @@ Lazy evaluation in the Nix language allows the module system to make a value ava The result of this represents the list of command line arguments to pass to the `./map` script. ## Conditional Definitions -Sometimes, you will want option values to be, well, optional. This can be useful when defining a value for an option is not required, as in the following case. +Sometimes, you will want option values to be, well, optional. This can be useful when defining a value for an option is not required, as in the following case. You will define a new option, `map.zoom`, to control the zoom level of the map. The Google Maps API will infer a zoom level if no corresponding argument is passed, a situation you can represent with the `nullOr `, which represents values of type `` or `null`. This means that when the option isn't defined, the value of such an option is `null`, a value that can be checked against in a conditional. From 86e86f931dc73651f0672f9e0c875a969839599c Mon Sep 17 00:00:00 2001 From: Lorenzo Manacorda Date: Fri, 3 Nov 2023 10:32:14 +0100 Subject: [PATCH 35/36] module-system.md: replace comments with captions --- .../tutorials/module-system/module-system.md | 210 +++++++++--------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 47f776353c..095ccbd76d 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -49,8 +49,8 @@ The simplest module is just a function that takes any attributes and returns an Write the following into a file called `default.nix`: -```nix -# default.nix +```{code-block} nix +:caption: default.nix { ... }: { @@ -62,8 +62,8 @@ Write the following into a file called `default.nix`: We will need some helper functions, which will come from the Nixpkgs library. Start by changing the first line in `default.nix`: -```diff -# default.nix +```{code-block} diff +:caption: default.nix - { ... }: + { lib, ... }: { @@ -91,8 +91,8 @@ In this section, you will define the `scripts.output` option. Change `default.nix` to include the following declaration: -```diff -# default.nix +```{code-block} diff +:caption: default.nix { lib, ... }: { + options = { @@ -118,8 +118,8 @@ Here we use `scripts`, because we will add another script later, and call this o Write a new file, `eval.nix`, which you will use to evaluate `default.nix`: -```nix -# eval.nix +```{code-block} nix +:caption: eval.nix let nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-22.11"; pkgs = import nixpkgs { config = {}; overlays = []; }; @@ -163,8 +163,8 @@ What happens if you instead try to assign an integer to the option? Add the following lines to `default.nix`: -```diff -# default.nix +```{code-block} diff +:caption: default.nix { lib, ... }: { options = { @@ -201,8 +201,8 @@ The output is passed on to display it with [`feh`](https://feh.finalrewind.org/) Update `default.nix` by changing the value of `scripts.output` to the following string: -```diff -# default.nix +```{code-block} diff +:caption: default.nix config = { - scripts.output = 42; + scripts.output = '' @@ -218,8 +218,8 @@ We can solve this by packaging the raw {download}`map ` script with ` First, make available a `pkgs` argument in your module evaluation by adding a module that sets `config._module.args`: -```diff -# eval.nix +```{code-block} diff +:caption: eval.nix pkgs.lib.evalModules { modules = [ + ({ config, ... }: { config._module.args = { inherit pkgs; }; }) @@ -234,8 +234,8 @@ This mechanism is currently only [documented in the module system code](https:// Then change `default.nix` to have the following contents: -```nix -# default.nix +```{code-block} nix +:caption: default.nix { pkgs, lib, ... }: { options = { @@ -304,8 +304,8 @@ Module option types not only check for valid values, but also specify how multip Make the following additions to your `default.nix` file: -```diff -# default.nix +```{code-block} diff +:caption: default.nix scripts.output = lib.mkOption { type = lib.types.package; }; @@ -346,8 +346,8 @@ To make option values available to a module, the arguments of the function decla Update `default.nix` to add the `config` attribute: -```diff -# default.nix +```{code-block} diff +:caption: default.nix -{ pkgs, lib, ... }: { +{ pkgs, lib, config, ... }: { ``` @@ -369,8 +369,8 @@ The `config` *argument* is **not** the same as the `config` *attribute*: Now make the following changes to `default.nix`: -```diff -# default.nix +```{code-block} diff +:caption: default.nix config = { scripts.output = pkgs.writeShellApplication { name = "map"; @@ -399,8 +399,8 @@ You will define a new option, `map.zoom`, to control the zoom level of the map. Add the `map` attribute set with the `zoom` option into the top-level `options` declaration, like so: -```diff -# default.nix +```{code-block} diff +:caption: default.nix requestParams = lib.mkOption { type = lib.types.listOf lib.types.str; }; @@ -416,8 +416,8 @@ Add the `map` attribute set with the `zoom` option into the top-level `options` To make use of this, use the `mkIf ` function, which only adds the definition if the condition evaluates to `true`. Make the following additions to the `requestParams` list in the `config` block: -```diff -# default.nix +```{code-block} diff +:caption: default.nix requestParams = [ "size=640x640" "scale=2" @@ -438,8 +438,8 @@ Its value will be used if the value of the option declaring it is not specified Add the corresponding line: -```diff -# default.nix +```{code-block} diff +:caption: default.nix map = { zoom = lib.mkOption { type = lib.types.nullOr lib.types.int; @@ -455,8 +455,8 @@ You have now declared options controlling the map dimensions and zoom level, but Add the `center` option now, possibly with your own location as default value: -```diff -# default.nix +```{code-block} diff +:caption: default.nix type = lib.types.nullOr lib.types.int; default = 2; }; @@ -475,8 +475,8 @@ There are multiple ways of making a new package accessible, but as an exercise, First, add a new option to accommodate the package: -```diff -# default.nix +```{code-block} diff +:caption: default.nix options = { scripts.output = lib.mkOption { type = lib.types.package; @@ -489,8 +489,8 @@ First, add a new option to accommodate the package: Then define the value for that option where you make the raw script reproducible by wrapping a call to it in `writeShellApplication`: -```diff -# default.nix +```{code-block} diff +:caption: default.nix config = { + scripts.geocode = pkgs.writeShellApplication { + name = "geocode"; @@ -505,8 +505,8 @@ Then define the value for that option where you make the raw script reproducible Add another `mkIf` call to the list of `requestParams` now where you access the wrapped package through `config.scripts.geocode`, and run the executable `/bin/geocode` inside: -```diff -# default.nix +```{code-block} diff +:caption: default.nix "scale=2" (lib.mkIf (config.map.zoom != null) "zoom=${toString config.map.zoom}") @@ -530,8 +530,8 @@ In particular, this allows you to separate option declarations from where they a Create a new module, `marker.nix`, where you can declare options for defining location pins and other markers on the map: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix { lib, config, ... }: { } @@ -539,8 +539,8 @@ Create a new module, `marker.nix`, where you can declare options for defining lo Reference this new file in `default.nix` using the `imports` attribute: -```diff -# default.nix +```{code-block} diff +:caption: default.nix { pkgs, lib, config ... }: { + imports = [ @@ -563,8 +563,8 @@ Each assignment of markers will be type-checked during evaluation of the top-lev Make the following changes to `marker.nix`: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix -{ pkgs, lib, config, ... }: { +{ pkgs, lib, config, ... }: +let @@ -593,8 +593,8 @@ In this case, you will use the `map.markers` option to produce and add new eleme To implement this behavior, add the following `config` block to `marker.nix`: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix + config = { + + map.markers = [ @@ -631,8 +631,8 @@ When defining multiple markers, determining an appropriate center or zoom level To achieve this, make the following additions to `marker.nix`, above the `requestParams` declaration: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix + map.center = lib.mkIf + (lib.length config.map.markers >= 1) + null; @@ -662,8 +662,8 @@ To propagate marker definitions from `users` to the `map.markers` option, make t In the `let` block: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix + userType = lib.types.submodule { + options = { + departure = lib.mkOption { @@ -680,8 +680,8 @@ This defines a submodule type for a user, with a `departure` option of type `mar In the `options` block, above `map.markers`: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix + users = lib.mkOption { + type = lib.types.attrsOf userType; + }; @@ -691,8 +691,8 @@ That allows adding a `users` attribute set to `config` in any submodule that imp In the `config` block, above `map.center`: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix config = { - map.markers = [ @@ -730,8 +730,8 @@ You can implement this with the `strMatching ""` type, where `` is In the `let` block: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix type = lib.types.nullOr lib.types.str; default = null; }; @@ -749,8 +749,8 @@ Again, `types.nullOr` allows for `null` values, and the default has been set to In the `paramForMarker` function: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix paramForMarker = marker: let attributes = @@ -773,8 +773,8 @@ But since every `users` attribute has a name, we could use that as an automatic This `firstUpperAlnum` function allows you to retrieve the first character of the username, with the correct type for passing to `departure.style.label`: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix { lib, config, ... }: let + # Returns the uppercased first letter @@ -792,8 +792,8 @@ By transforming the argument to `lib.types.submodule` into a function, you can a One special argument automatically available to submodules is `name`, which when used in `attrsOf`, gives you the name of the attribute the submodule is defined under: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix - userType = lib.types.submodule { + userType = lib.types.submodule ({ name, ... }: { options = { @@ -809,8 +809,8 @@ In this case, you don't easily have access to the name from the marker submodule Instead you can use the `config` section of the `user` submodule to set a default, like so: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix + + config = { + departure.style.label = lib.mkDefault @@ -841,8 +841,8 @@ Here you will use two new type-functions for this: In the `let` block, add the following `colorType` option, which can hold strings containing either some given color names or an RGB value add the new compound type: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix ... (builtins.match "[^A-Z0-9]*([A-Z0-9]).*" (lib.toUpper str)); @@ -863,8 +863,8 @@ This allows either strings that matche a 24-bit hexadecimal number or are equal At the bottom of the `let` block, add the `style.color` option and specify a default value: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix (lib.types.strMatching "[A-Z0-9]"); default = null; }; @@ -879,8 +879,8 @@ At the bottom of the `let` block, add the `style.color` option and specify a def Now add an entry to the `paramForMarker` list which makes use of the new option: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix (marker.style.label != null) "label:${marker.style.label}" ++ [ @@ -896,8 +896,8 @@ In case you set many different markers, it would be helpful to have the ability Add a new `style.size` option to `marker.nix`, allowing you to choose from the set of pre-defined sizes: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix type = colorType; default = "red"; }; @@ -913,8 +913,8 @@ Add a new `style.size` option to `marker.nix`, allowing you to choose from the s Now add a mapping for the size parameter in `paramForMarker`, which selects an appropriate string to pass to the API: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix requestParams = let paramForMarker = marker: let @@ -930,7 +930,7 @@ Now add a mapping for the size parameter in `paramForMarker`, which selects an a Finally, add another `lib.optional` call to the `attributes` string, making use of the selected size: ``` -# marker.nix +:caption: marker.nix attributes = lib.optional (marker.style.label != null) @@ -953,8 +953,8 @@ The new option defined in the next section will allow you to set an *arrival* ma To start, create a new `path.nix` file with the following contents: -```nix -# path.nix +```{code-block} nix +:caption: path.nix { lib, config, ... }: let pathType = lib.types.submodule { @@ -994,8 +994,8 @@ In the `config` attribute we augment the API call by setting the `requestParams` Now import this new `path.nix` module from your `marker.nix` module: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix in { + imports = [ @@ -1011,8 +1011,8 @@ Now import this new `path.nix` module from your `marker.nix` module: Copy the `departure` option declaration to a new `arrival` option in `marker.nix`, to complete the initial path implementation: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix type = markerType; default = {}; }; @@ -1026,8 +1026,8 @@ Copy the `departure` option declaration to a new `arrival` option in `marker.nix Next, add an `arrival.style.label` attribute to the `config` block, mirroring the `departure.style.label`: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix config = { departure.style.label = lib.mkDefault (firstUpperAlnum name); @@ -1039,8 +1039,8 @@ Next, add an `arrival.style.label` attribute to the `config` block, mirroring th Finally, update the return list in the function passed to `concatMap` in `map.markers` to also include the `arrival` marker for each user: -```diff -# marker.nix +```{code-block} diff +:caption: marker.nix map.markers = lib.filter (marker: marker.location != null) (lib.concatMap (user: [ @@ -1057,8 +1057,8 @@ Now you have the basesis to define paths on the map, connecting pairs of departu In the path module, define a path connecting every user's departure and arrival locations: -```diff -# path.nix +```{code-block} diff +:caption: path.nix config = { + + map.paths = builtins.map (user: { @@ -1089,8 +1089,8 @@ As before, you'll now declare a new submodule for the path style. While you could also directly declare the `style.weight` option, in this case you should use the submodule to be able reuse the path style type later. Add the `pathStyleType` submodule option to the `let` block in `path.nix`: -```diff -# path.nix +```{code-block} diff +:caption: path.nix { lib, config, ... }: let + @@ -1114,8 +1114,8 @@ The path weight will default to 5, but can be set to any integer value in the 1 Now add a `style` option to the `options` set further down the file: -```diff -# path.nix +```{code-block} diff +:caption: path.nix options = { locations = lib.mkOption { type = lib.types.listOf lib.types.str; @@ -1132,8 +1132,8 @@ Now add a `style` option to the `options` set further down the file: Finally, update the `attributes` list in `paramForPath`: -```diff -# path.nix +```{code-block} diff +:caption: path.nix paramForPath = path: let attributes = @@ -1156,8 +1156,8 @@ The module system allows you to declare values for an option multiple times, and This makes it possible to have a definition for the `user` option in the `marker.nix` module, as well as a `user` definition in `path.nix`: -```diff -# path.nix +```{code-block} diff +:caption: path.nix in { options = { + @@ -1177,8 +1177,8 @@ This makes it possible to have a definition for the `user` option in the `marker Then add a line using the `user.pathStyle` option in `map.paths` where each user's paths are processed: -```diff -# path.nix +```{code-block} diff +:caption: path.nix user.departure.location user.arrival.location ]; @@ -1196,8 +1196,8 @@ You can accomplish this using types you've already encountered by now. Add a new `colorType` block to `path.nix`, specifying the allowed color names and RGB/RGBA hexadecimal values: -```diff -# path.nix +```{code-block} diff +:caption: path.nix { lib, config, ... }: let @@ -1214,8 +1214,8 @@ Add a new `colorType` block to `path.nix`, specifying the allowed color names an Under the `weight` option, add a new `color` option to use the new `colorType` value: -```diff -# path.nix +```{code-block} diff +:caption: path.nix type = lib.types.ints.between 1 20; default = 5; }; @@ -1230,8 +1230,8 @@ Under the `weight` option, add a new `color` option to use the new `colorType` v Finally, add a line using the `color` option to the `attributes` list: -```diff -# path.nix +```{code-block} diff +:caption: path.nix attributes = [ "weight:${toString path.style.weight}" @@ -1249,8 +1249,8 @@ Since this feature can be turned on or off, you can do this using the `bool` typ Make the following changes to `path.nix` now: -```diff -# path.nix +```{code-block} diff +:caption: path.nix type = colorType; default = "blue"; }; @@ -1265,8 +1265,8 @@ Make the following changes to `path.nix` now: Make sure to also add a line to use that value in `attributes` list, so the option value is included in the API call: -```diff -# path.nix +```{code-block} diff +:caption: path.nix [ "weight:${toString path.style.weight}" "color:${path.style.color}" From 3c3b4b286be52e17aa6fac3d5ebf8c0254161cad Mon Sep 17 00:00:00 2001 From: Lorenzo Manacorda Date: Fri, 3 Nov 2023 13:00:43 +0100 Subject: [PATCH 36/36] wip emph --- .../tutorials/module-system/module-system.md | 144 ++++++++++-------- 1 file changed, 83 insertions(+), 61 deletions(-) diff --git a/source/tutorials/module-system/module-system.md b/source/tutorials/module-system/module-system.md index 095ccbd76d..d7c233bad9 100644 --- a/source/tutorials/module-system/module-system.md +++ b/source/tutorials/module-system/module-system.md @@ -62,10 +62,10 @@ Write the following into a file called `default.nix`: We will need some helper functions, which will come from the Nixpkgs library. Start by changing the first line in `default.nix`: -```{code-block} diff +```{code-block} nix :caption: default.nix -- { ... }: -+ { lib, ... }: +:emphasize-lines: 1 +{ lib, ... }: { } @@ -91,17 +91,18 @@ In this section, you will define the `scripts.output` option. Change `default.nix` to include the following declaration: -```{code-block} diff +```{code-block} nix :caption: default.nix - { lib, ... }: { +:emphasize-lines: 3-7 +{ lib, ... }: { -+ options = { -+ scripts.output = lib.mkOption { -+ type = lib.types.lines; -+ }; -+ }; + options = { + scripts.output = lib.mkOption { + type = lib.types.lines; + }; + }; - } +} ``` While many attributes for customizing options are available, the most important one is `type`, which specifies which values are valid for an option. @@ -163,9 +164,10 @@ What happens if you instead try to assign an integer to the option? Add the following lines to `default.nix`: -```{code-block} diff +```{code-block} nix :caption: default.nix - { lib, ... }: { +:emphasize-lines: 9-11 +{ lib, ... }: { options = { scripts.output = lib.mkOption { @@ -173,10 +175,10 @@ Add the following lines to `default.nix`: }; }; -+ config = { -+ scripts.output = 42; -+ }; - } + config = { + scripts.output = 42; + }; +} ``` Now try to execute the previous command, and witness your first module error: @@ -201,14 +203,19 @@ The output is passed on to display it with [`feh`](https://feh.finalrewind.org/) Update `default.nix` by changing the value of `scripts.output` to the following string: -```{code-block} diff +```{code-block} nix :caption: default.nix - config = { -- scripts.output = 42; -+ scripts.output = '' -+ ./map size=640x640 scale=2 | feh - -+ ''; - }; +:emphasize-lines: 6-8 +{ lib, ... }: { + + # ... + + config = { + scripts.output = '' + ./map size=640x640 scale=2 | feh - + ''; + }; +}; ``` ## Interlude: Reproducible scripts @@ -218,12 +225,14 @@ We can solve this by packaging the raw {download}`map ` script with ` First, make available a `pkgs` argument in your module evaluation by adding a module that sets `config._module.args`: -```{code-block} diff +```{code-block} nix :caption: eval.nix +:emphasize-lines: 4 +{ lib, ... }: { pkgs.lib.evalModules { modules = [ -+ ({ config, ... }: { config._module.args = { inherit pkgs; }; }) - ./test.nix + ({ config, ... }: { config._module.args = { inherit pkgs; }; }) + ./default.nix ]; } ``` @@ -304,16 +313,21 @@ Module option types not only check for valid values, but also specify how multip Make the following additions to your `default.nix` file: -```{code-block} diff +```{code-block} nix :caption: default.nix - scripts.output = lib.mkOption { - type = lib.types.package; - }; -+ -+ requestParams = lib.mkOption { -+ type = lib.types.listOf lib.types.str; -+ }; - }; +:emphasize-lines: 9-11,23-26 +{ lib, ... }: { + + # ... + + scripts.output = lib.mkOption { + type = lib.types.package; + }; + + requestParams = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; + }; config = { scripts.output = pkgs.writeShellApplication { @@ -323,11 +337,11 @@ Make the following additions to your `default.nix` file: ${./map} size=640x640 scale=2 | feh - ''; }; -+ -+ requestParams = [ -+ "size=640x640" -+ "scale=2" -+ ]; + + requestParams = [ + "size=640x640" + "scale=2" + ]; }; } ``` @@ -346,10 +360,9 @@ To make option values available to a module, the arguments of the function decla Update `default.nix` to add the `config` attribute: -```{code-block} diff +```{code-block} nix :caption: default.nix --{ pkgs, lib, ... }: { -+{ pkgs, lib, config, ... }: { +{ pkgs, lib, config, ... }: { ``` When a module that sets options is evaluated, the resulting values can be accessed by their corresponding attribute names under `config`. @@ -369,17 +382,20 @@ The `config` *argument* is **not** the same as the `config` *attribute*: Now make the following changes to `default.nix`: -```{code-block} diff +```{code-block} nix :caption: default.nix - config = { - scripts.output = pkgs.writeShellApplication { - name = "map"; - runtimeInputs = with pkgs; [ curl feh ]; - text = '' -- ${./map} size=640x640 scale=2 | feh - -+ ${./map} ${lib.concatStringsSep " " -+ config.requestParams} | feh - - ''; +:emphasize-lines: 7,8 +{ lib, ... }: { + config = { + scripts.output = pkgs.writeShellApplication { + name = "map"; + runtimeInputs = with pkgs; [ curl feh ]; + text = '' + ${./map} ${lib.concatStringsSep " " + config.requestParams} | feh - + ''; + + # ... ``` Here, the value of the `config.requestParams` attribute is populated by the module system based on the definitions in the same file. @@ -399,18 +415,24 @@ You will define a new option, `map.zoom`, to control the zoom level of the map. Add the `map` attribute set with the `zoom` option into the top-level `options` declaration, like so: -```{code-block} diff +```{code-block} nix :caption: default.nix +:emphasize-lines: 8-12 +{ lib, ... }: { + # ... + requestParams = lib.mkOption { type = lib.types.listOf lib.types.str; }; -+ -+ map = { -+ zoom = lib.mkOption { -+ type = lib.types.nullOr lib.types.int; -+ }; -+ }; + + map = { + zoom = lib.mkOption { + type = lib.types.nullOr lib.types.int; + }; + }; }; + + # ... ``` To make use of this, use the `mkIf ` function, which only adds the definition if the condition evaluates to `true`.