1{ config, lib, pkgs, utils, ... }:
2
3let
4 # The scripted initrd contains some magic to add the prefix to the
5 # paths just in time, so we don't add it here.
6 sysrootPrefix = fs:
7 if config.boot.initrd.systemd.enable && (utils.fsNeededForBoot fs) then
8 "/sysroot"
9 else
10 "";
11
12 # Returns a service that creates the required directories before the mount is
13 # created.
14 preMountService = _name: fs:
15 let
16 prefix = sysrootPrefix fs;
17
18 escapedMountpoint = utils.escapeSystemdPath (prefix + fs.mountPoint);
19 mountUnit = "${escapedMountpoint}.mount";
20
21 upperdir = prefix + fs.overlay.upperdir;
22 workdir = prefix + fs.overlay.workdir;
23 in
24 lib.mkIf (fs.overlay.upperdir != null)
25 {
26 "rw-${escapedMountpoint}" = {
27 requiredBy = [ mountUnit ];
28 before = [ mountUnit ];
29 unitConfig = {
30 DefaultDependencies = false;
31 RequiresMountsFor = "${upperdir} ${workdir}";
32 };
33 serviceConfig = {
34 Type = "oneshot";
35 ExecStart = "${pkgs.coreutils}/bin/mkdir -p -m 0755 ${upperdir} ${workdir}";
36 };
37 };
38 };
39
40 overlayOpts = { config, ... }: {
41
42 options.overlay = {
43
44 lowerdir = lib.mkOption {
45 type = with lib.types; nullOr (nonEmptyListOf (either str pathInStore));
46 default = null;
47 description = ''
48 The list of path(s) to the lowerdir(s).
49
50 To create a writable overlay, you MUST provide an upperdir and a
51 workdir.
52
53 You can create a read-only overlay when you provide multiple (at
54 least 2!) lowerdirs and neither an upperdir nor a workdir.
55 '';
56 };
57
58 upperdir = lib.mkOption {
59 type = lib.types.nullOr lib.types.str;
60 default = null;
61 description = ''
62 The path to the upperdir.
63
64 If this is null, a read-only overlay is created using the lowerdir.
65
66 If you set this to some value you MUST also set `workdir`.
67 '';
68 };
69
70 workdir = lib.mkOption {
71 type = lib.types.nullOr lib.types.str;
72 default = null;
73 description = ''
74 The path to the workdir.
75
76 This MUST be set if you set `upperdir`.
77 '';
78 };
79
80 };
81
82 config = lib.mkIf (config.overlay.lowerdir != null) {
83 fsType = "overlay";
84 device = lib.mkDefault "overlay";
85
86 options =
87 let
88 prefix = sysrootPrefix config;
89
90 lowerdir = map (s: prefix + s) config.overlay.lowerdir;
91 upperdir = prefix + config.overlay.upperdir;
92 workdir = prefix + config.overlay.workdir;
93 in
94 [
95 "lowerdir=${lib.concatStringsSep ":" lowerdir}"
96 ] ++ lib.optionals (config.overlay.upperdir != null) [
97 "upperdir=${upperdir}"
98 "workdir=${workdir}"
99 ] ++ (map (s: "x-systemd.requires-mounts-for=${s}") lowerdir);
100 };
101
102 };
103in
104
105{
106
107 options = {
108
109 # Merge the overlay options into the fileSystems option.
110 fileSystems = lib.mkOption {
111 type = lib.types.attrsOf (lib.types.submodule [ overlayOpts ]);
112 };
113
114 };
115
116 config =
117 let
118 overlayFileSystems = lib.filterAttrs (_name: fs: (fs.overlay.lowerdir != null)) config.fileSystems;
119 initrdFileSystems = lib.filterAttrs (_name: utils.fsNeededForBoot) overlayFileSystems;
120 userspaceFileSystems = lib.filterAttrs (_name: fs: (!utils.fsNeededForBoot fs)) overlayFileSystems;
121 in
122 {
123
124 boot.initrd.availableKernelModules = lib.mkIf (initrdFileSystems != { }) [ "overlay" ];
125
126 assertions = lib.concatLists (lib.mapAttrsToList
127 (_name: fs: [
128 {
129 assertion = (fs.overlay.upperdir == null) == (fs.overlay.workdir == null);
130 message = "You cannot define a `lowerdir` without a `workdir` and vice versa for mount point: ${fs.mountPoint}";
131 }
132 {
133 assertion = (fs.overlay.lowerdir != null && fs.overlay.upperdir == null) -> (lib.length fs.overlay.lowerdir) >= 2;
134 message = "A read-only overlay (without an `upperdir`) requires at least 2 `lowerdir`s: ${fs.mountPoint}";
135 }
136 ])
137 config.fileSystems);
138
139 boot.initrd.systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService initrdFileSystems);
140 systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService userspaceFileSystems);
141
142 };
143
144}