Skip to content

Commit

Permalink
Module system *introductory* tutorial (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
srid authored Feb 27, 2024
1 parent 44174da commit e781db5
Show file tree
Hide file tree
Showing 16 changed files with 642 additions and 1 deletion.
21 changes: 21 additions & 0 deletions .github/workflows/code.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: "Code"
on:
push:
branches:
- master
pull_request:
jobs:
code:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
with:
extra-conf: |
trusted-public-keys = cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
substituters = https://cache.garnix.io?priority=41 https://cache.nixos.org/
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: yaxitech/nix-install-pkgs-action@v3
with:
packages: "github:srid/nixci"
- run: nixci
168 changes: 168 additions & 0 deletions en/nix-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
---
order: 3
---

# Introduction to module system

Using the [[modules|module system]] is a key stepping stone to writing maintainable and shareable [[nix|Nix]] code. In this tutorial, we'll write a configuration system for the simple [lsd] command, thus *introducing* the reader to the Nix [[modules|module system]], so that they benefit from features such as configuration type checking, option documentation, and modularity. To learn more about the module system, we recommend [this video from Tweag](https://www.youtube.com/watch?v=N7hFP_40DJo) as well the article "[Module system deep dive][doc]" from nix.dev.

We shall begin by understanding the low-levels: how to use `evalModules` from [[nixpkgs|nixpkgs]] to define and use our own modules from scratch, using the aforementioned `lsd` use-case. The next tutorial in this series will go one high-level up and talk about how to work with modules across [[flakes|flakes]], using [[flake-parts]].

[doc]: https://nix.dev/tutorials/module-system/module-system.html

## A simple example

Consider the following Nix code, defined in a [[flakes|flake]]:

[[nix-modules/1/flake.nix]]
![[nix-modules/1/flake.nix]]

>[!info] Source code for this tutorial
> All source code for the Nix in this tutorial is available [here](https://github.com/nixos-asia/website/tree/master/global/nix-modules).
This is a simple flake that exposes a package (a [[writeShellApplication]] [[drv]] wrapping [lsd]), that can be [[nix-first|`nix run`ed]] to list the contents of the root directory.

```sh
❯ nix run
drwxrwxr-x root admin 2.5 KB Tue Jan 30 15:19:06 2024  Applications
drwxr-xr-x root wheel 1.2 KB Sat Nov 18 23:43:59 2023  bin
dr-xr-xr-x root wheel 5.1 KB Wed Jan 17 09:21:57 2024  dev
lrwxr-xr-x root wheel 11 B Sat Nov 18 23:43:59 2023  etc ⇒ private/etc
lrwxr-xr-x root wheel 25 B Wed Jan 17 09:22:56 2024  home ⇒ /System/Volumes/Data/home
drwxr-xr-x root wheel 2.2 KB Mon Dec 4 02:08:02 2023  Library
drwxr-xr-x root wheel 224 B Sat Jul 22 20:09:12 2023  nix
...
```

This program is hardcoded to do a certain thing: it can list the contents of the `/` directory. Now let's say we want to configure its behaviour but without having to modify the derivation itself.

In particular, we want our program to:
- *list a different directory*.
- or, *show a tree view rather than a linear list*.

Normally we can achieve this by refactoring our Nix expression to be a *function* (see `lsdFor` ⤵️) that takes arguments for these variations (`dir` and `tree` ⤵️), producing the appropriate [[drv|derivation]] as a result:

[[nix-modules/2/flake.nix]]
![[nix-modules/2/flake.nix]]

Now we can try out each of these variations:

```sh
❯ nix run .#home
 code  Documents  Keybase  Movies  org ...

❯ nix run .#downloads
 Downloads
├──  '$RECYCLE.BIN'
│ └──  desktop.ini
├──  2303.18223.pdf
├──  4.jpg
├──  '[ORIGINAL] PKD MASTERY GUIDE BOOK.pdf'
├──  'ACTUAL FREEDOM'
│ ├──  'ACTUAL FREEDOM (1).txt'
│ └──  "ACTUAL FREEDOM (Richard's Words Only).txt"
...
```

The `lsdFor` function returns a `lsd` wrapper package that behaves in accordance with the arguments we pass to it. The flake outputs three packages, including one for listing the user's home directory as well as their "Downloads" folder as a tree view.

>[!tip] Case for the `lsd` module
> Our above flake is simple enough that it strictly doesn't require further refactoring. However, in larger flakes, having functions peppered throughout the project can be rather difficult to entangle; besides, we want to modular overrides and type checking, along with documentation. To this end, we'll see how to refactor the above to use the module system, and in the process we'll add more configurability to our `lsd` wrapper.
{#introduce}
## Introducing the module system

1. A Nix *module* is a specification of various `options`.
1. When the user `imports` this module, they can assign these options.
1. The module implementation (ie., the `config` attribute) will then use these values to produce the final expression to substitute in call site where the module gets imported.

Modules can import each other in nested fashion; and option types can have certain merge semantics allowing you to define the same option across multiple modules.

This is a mouthful, so let's get down to the concrete details. To port our flake above, we need to define two options: `dir`, and `tree`. We will as well add a third option that is not user-setable but will be used set the resulting package.

Here's our lsd module, defined in `lsd.nix` alongside the flake. Follow along the code comments:

[[nix-modules/3/lsd.nix]]
![[nix-modules/3/lsd.nix]]

>[!info] Follow the comments
> We recommend that you follow the comments in the above Nix file to understand its structure. As always, consult [Module system deep dive][doc] to learn of all the details.
Note:

- `mkOption` is used create the option *types*
- Types used here: *str*, *bool*, *package* and *submodule*
- A "submodule" is a nested module, with its own options/ imports and config.
- `config` gives the implementation when the user sets the options.
- In our case, we 'output' the result in the `package` option (which cannot be set by the user, due to `readOnly = true`).

Let's evaluate it from the [[repl]]:

```sh
❯ nix repl
Welcome to Nix 2.19.2. Type :? for help.

nix-repl> :lf nixpkgs
Added 15 variables.

nix-repl> pkgs = legacyPackages.${builtins.currentSystem}

nix-repl> lib = pkgs.lib

nix-repl> res = lib.evalModules { modules = [ ./lsd.nix { lsd.dir = "$HOME"; } ]; specialArgs = { inherit pkgs; }; }

nix-repl> res.config.lsd.package
«derivation /nix/store/my26y1wp6801sslfvfzf21q41fzh8bch-list-contents.drv»

nix-repl> :b res.config.lsd.package
This derivation produced the following outputs:
out -> /nix/store/m8phgz5ch7whqbs5pk991pc0cfczsghk-list-contents
```

Using `evalModules`, as we saw in the repl session, we can refactor our previous flake:

[[nix-modules/3/flake.nix]]
![[nix-modules/3/flake.nix]]

>[!tip] Hmm!
> You may notice that there's not much difference. If anything our new flake is *slightly* more complex, due to use of `evalModules`. The simplicity of the module system will become evident as you write more complex flakes, or if you want to share your modules or override them.
{#imports}
## Importing modules

Let's do something more interesting in the above flake. We'll create a "common settings" module, and then use that across the packages using the `imports` attribute. `evalModules` implements a type merge system that knows how to merge same attributes from multiple modules.

[[nix-modules/4/flake.nix]]
![[nix-modules/4/flake.nix]]

Compared to the 3rd flake, we have:

- In [[nix-modules/4/lsd.nix]]: a new option `long` to specify `-l` to lsd.
- In [[nix-modules/4/flake.nix]]:
- a new module `common` enabling the `long` option.
- all packages now `imports` this common module, to derive the `long` option.
- a `mkLib` functions that we will export for reuse from another flake (see below)

Now when you `nix run` these programs you will get similar output to the previous flake but with a long listing instead.

{#share}
## Sharing modules across flakes

We will create a 5th flake that re-uses module from the 4th flake above. This is a contrived example, but it demonstrates how you can share modules across flakes.

[[nix-modules/5/flake.nix]]
![[nix-modules/5/flake.nix]]

Note that,

- [[nix-modules/4/flake.nix]] outputs a `mkLib` function that gives us the `common` module along with the `lsdFor` function.
- In [[nix-modules/5/flake.nix]], we access these for re-use, thus relieving our 5th flake of having to define `lsd.nix` and the `common` module.

Our 5th flake is fairly simple, due to hiding all the implementation in an external flake (4th flake). The 5th flake contains only the "what" and not the "how" of our `lsd` packages; it tells us what to configure, hiding the implementation in an input flake (4th flake).

{#end}
## Where to go from here?

You have just read a quick introduction to the module system, in particular how to define, use and share them in [[flakes]]. To learn more about the module system, we recommend [this video from Tweag](https://www.youtube.com/watch?v=N7hFP_40DJo) as well the article "[Module system deep dive][doc]" from nix.dev. Look out for the next tutorial in this series, where we will talk about [[flake-parts]].

[lsd]: https://github.com/lsd-rs/lsd
2 changes: 1 addition & 1 deletion en/nix-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ order: -10

- [x] [[nix-first]]#
- [x] [[nix-rapid]]#
- [ ] `evalModules`
- [ ] [[nix-modules]]#
- [ ] `flake-parts`
10 changes: 10 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,15 @@
apps.default.program = self'.apps.en.program; # Alias to English site
formatter = pkgs.nixpkgs-fmt;
};
flake.nixci.default = {
nix-modules-1.dir = ./global/nix-modules/1;
nix-modules-2.dir = ./global/nix-modules/2;
nix-modules-3.dir = ./global/nix-modules/3;
nix-modules-4.dir = ./global/nix-modules/4;
nix-modules-5 = {
dir = ./global/nix-modules/5;
overrideInputs.flake4 = ./global/nix-modules/4;
};
};
};
}
27 changes: 27 additions & 0 deletions global/nix-modules/1/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions global/nix-modules/1/flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
# TODO: Change this to x86_64-linux if you are on Linux
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.${system}.default = pkgs.writeShellApplication {
name = "list-contents";
runtimeInputs = [ pkgs.lsd ];
text = ''
lsd -l /
'';
};
};
}
27 changes: 27 additions & 0 deletions global/nix-modules/2/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions global/nix-modules/2/flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
# TODO: Change this to x86_64-linux if you are on Linux
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
# ⤵️ We introduced a function here
lsdFor = { dir, tree ? false }: pkgs.writeShellApplication {
name = "list-contents";
runtimeInputs = [ pkgs.lsd ];
text = ''
lsd ${if tree then "--tree" else ""} "${dir}"
'';
};
in
{
packages.${system} = {
# ⤵️ And call that function here
default = lsdFor { dir = "/"; };
home = lsdFor { dir = "$HOME"; };
downloads = lsdFor { dir = "$HOME/Downloads"; tree = true; };
};
};
}
27 changes: 27 additions & 0 deletions global/nix-modules/3/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions global/nix-modules/3/flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
# TODO: Change this to x86_64-linux if you are on Linux
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
lib = pkgs.lib;
lsdFor = settings:
let
result = lib.evalModules {
modules = [
# Note that 'settings' is no different to the lsd.nix module.
./lsd.nix
settings
];
# Arguments passed here become automatically available to all
# modules.
specialArgs = { inherit pkgs; };
};
in
result.config.lsd.package;
in
{
packages.${system} = {
default = lsdFor { lsd.dir = "/"; };
home = lsdFor { lsd.dir = "$HOME"; };
downloads = lsdFor { lsd.dir = "$HOME/Downloads"; lsd.tree = true; };
};
};
}
Loading

0 comments on commit e781db5

Please sign in to comment.