1{ config, lib, pkgs, utils, ... }:
2
3with lib;
4with utils;
5
6let
7
8 addCheckDesc = desc: elemType: check: types.addCheck elemType check
9 // { description = "${elemType.description} (with check: ${desc})"; };
10
11 isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null;
12 nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty;
13
14 fileSystems' = toposort fsBefore (attrValues config.fileSystems);
15
16 fileSystems = if fileSystems' ? result
17 then # use topologically sorted fileSystems everywhere
18 fileSystems'.result
19 else # the assertion below will catch this,
20 # but we fall back to the original order
21 # anyway so that other modules could check
22 # their assertions too
23 (attrValues config.fileSystems);
24
25 specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ];
26
27 nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str
28 (s: isNonEmpty s && (builtins.match ".+/" s) == null);
29
30 coreFileSystemOpts = { name, config, ... }: {
31
32 options = {
33 mountPoint = mkOption {
34 example = "/mnt/usb";
35 type = nonEmptyWithoutTrailingSlash;
36 description = "Location of the mounted the file system.";
37 };
38
39 device = mkOption {
40 default = null;
41 example = "/dev/sda";
42 type = types.nullOr nonEmptyStr;
43 description = "Location of the device.";
44 };
45
46 fsType = mkOption {
47 default = "auto";
48 example = "ext3";
49 type = nonEmptyStr;
50 description = "Type of the file system.";
51 };
52
53 options = mkOption {
54 default = [ "defaults" ];
55 example = [ "data=journal" ];
56 description = "Options used to mount the file system.";
57 type = types.listOf nonEmptyStr;
58 };
59
60 depends = mkOption {
61 default = [ ];
62 example = [ "/persist" ];
63 type = types.listOf nonEmptyWithoutTrailingSlash;
64 description = ''
65 List of paths that should be mounted before this one. This filesystem's
66 <option>device</option> and <option>mountPoint</option> are always
67 checked and do not need to be included explicitly. If a path is added
68 to this list, any other filesystem whose mount point is a parent of
69 the path will be mounted before this filesystem. The paths do not need
70 to actually be the <option>mountPoint</option> of some other filesystem.
71 '';
72 };
73
74 };
75
76 config = {
77 mountPoint = mkDefault name;
78 device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType);
79 };
80
81 };
82
83 fileSystemOpts = { config, ... }: {
84
85 options = {
86
87 label = mkOption {
88 default = null;
89 example = "root-partition";
90 type = types.nullOr nonEmptyStr;
91 description = "Label of the device (if any).";
92 };
93
94 autoFormat = mkOption {
95 default = false;
96 type = types.bool;
97 description = ''
98 If the device does not currently contain a filesystem (as
99 determined by <command>blkid</command>, then automatically
100 format it with the filesystem type specified in
101 <option>fsType</option>. Use with caution.
102 '';
103 };
104
105 formatOptions = mkOption {
106 default = "";
107 type = types.str;
108 description = ''
109 If <option>autoFormat</option> option is set specifies
110 extra options passed to mkfs.
111 '';
112 };
113
114 autoResize = mkOption {
115 default = false;
116 type = types.bool;
117 description = ''
118 If set, the filesystem is grown to its maximum size before
119 being mounted. (This is typically the size of the containing
120 partition.) This is currently only supported for ext2/3/4
121 filesystems that are mounted during early boot.
122 '';
123 };
124
125 noCheck = mkOption {
126 default = false;
127 type = types.bool;
128 description = "Disable running fsck on this filesystem.";
129 };
130
131 };
132
133 config = let
134 defaultFormatOptions =
135 # -F needed to allow bare block device without partitions
136 if (builtins.substring 0 3 config.fsType) == "ext" then "-F"
137 # -q needed for non-interactive operations
138 else if config.fsType == "jfs" then "-q"
139 # (same here)
140 else if config.fsType == "reiserfs" then "-q"
141 else null;
142 in {
143 options = mkIf config.autoResize [ "x-nixos.autoresize" ];
144 formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
145 };
146
147 };
148
149 # Makes sequence of `specialMount device mountPoint options fsType` commands.
150 # `systemMount` should be defined in the sourcing script.
151 makeSpecialMounts = mounts:
152 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: ''
153 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
154 '') mounts);
155
156in
157
158{
159
160 ###### interface
161
162 options = {
163
164 fileSystems = mkOption {
165 default = {};
166 example = literalExpression ''
167 {
168 "/".device = "/dev/hda1";
169 "/data" = {
170 device = "/dev/hda2";
171 fsType = "ext3";
172 options = [ "data=journal" ];
173 };
174 "/bigdisk".label = "bigdisk";
175 }
176 '';
177 type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]);
178 description = ''
179 The file systems to be mounted. It must include an entry for
180 the root directory (<literal>mountPoint = "/"</literal>). Each
181 entry in the list is an attribute set with the following fields:
182 <literal>mountPoint</literal>, <literal>device</literal>,
183 <literal>fsType</literal> (a file system type recognised by
184 <command>mount</command>; defaults to
185 <literal>"auto"</literal>), and <literal>options</literal>
186 (the mount options passed to <command>mount</command> using the
187 <option>-o</option> flag; defaults to <literal>[ "defaults" ]</literal>).
188
189 Instead of specifying <literal>device</literal>, you can also
190 specify a volume label (<literal>label</literal>) for file
191 systems that support it, such as ext2/ext3 (see <command>mke2fs
192 -L</command>).
193 '';
194 };
195
196 system.fsPackages = mkOption {
197 internal = true;
198 default = [ ];
199 description = "Packages supplying file system mounters and checkers.";
200 };
201
202 boot.supportedFilesystems = mkOption {
203 default = [ ];
204 example = [ "btrfs" ];
205 type = types.listOf types.str;
206 description = "Names of supported filesystem types.";
207 };
208
209 boot.specialFileSystems = mkOption {
210 default = {};
211 type = types.attrsOf (types.submodule coreFileSystemOpts);
212 internal = true;
213 description = ''
214 Special filesystems that are mounted very early during boot.
215 '';
216 };
217
218 };
219
220
221 ###### implementation
222
223 config = {
224
225 assertions = let
226 ls = sep: concatMapStringsSep sep (x: x.mountPoint);
227 notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs");
228 in [
229 { assertion = ! (fileSystems' ? cycle);
230 message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
231 }
232 { assertion = ! (any notAutoResizable fileSystems);
233 message = let
234 fs = head (filter notAutoResizable fileSystems);
235 in
236 "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${if fs.fsType == "auto" then " fsType has to be explicitly set and" else ""} only the ext filesystems and f2fs support it.";
237 }
238 ];
239
240 # Export for use in other modules
241 system.build.fileSystems = fileSystems;
242 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result;
243
244 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
245
246 # Add the mount helpers to the system path so that `mount' can find them.
247 system.fsPackages = [ pkgs.dosfstools ];
248
249 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages;
250
251 environment.etc.fstab.text =
252 let
253 fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" ];
254 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
255 # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
256 escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
257 swapOptions = sw: concatStringsSep "," (
258 sw.options
259 ++ optional (sw.priority != null) "pri=${toString sw.priority}"
260 ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}"
261 );
262 in ''
263 # This is a generated file. Do not edit!
264 #
265 # To make changes, edit the fileSystems and swapDevices NixOS options
266 # in your /etc/nixos/configuration.nix file.
267 #
268 # <file system> <mount point> <type> <options> <dump> <pass>
269
270 # Filesystems.
271 ${concatMapStrings (fs:
272 (if fs.device != null then escape fs.device
273 else if fs.label != null then "/dev/disk/by-label/${escape fs.label}"
274 else throw "No device specified for mount point ‘${fs.mountPoint}’.")
275 + " " + escape fs.mountPoint
276 + " " + fs.fsType
277 + " " + builtins.concatStringsSep "," fs.options
278 + " 0"
279 + " " + (if skipCheck fs then "0" else
280 if fs.mountPoint == "/" then "1" else "2")
281 + "\n"
282 ) fileSystems}
283
284 # Swap devices.
285 ${flip concatMapStrings config.swapDevices (sw:
286 "${sw.realDevice} none swap ${swapOptions sw}\n"
287 )}
288 '';
289
290 # Provide a target that pulls in all filesystems.
291 systemd.targets.fs =
292 { description = "All File Systems";
293 wants = [ "local-fs.target" "remote-fs.target" ];
294 };
295
296 systemd.services =
297
298 # Emit systemd services to format requested filesystems.
299 let
300 formatDevice = fs:
301 let
302 mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
303 device' = escapeSystemdPath fs.device;
304 device'' = "${device'}.device";
305 in nameValuePair "mkfs-${device'}"
306 { description = "Initialisation of Filesystem ${fs.device}";
307 wantedBy = [ mountPoint' ];
308 before = [ mountPoint' "systemd-fsck@${device'}.service" ];
309 requires = [ device'' ];
310 after = [ device'' ];
311 path = [ pkgs.util-linux ] ++ config.system.fsPackages;
312 script =
313 ''
314 if ! [ -e "${fs.device}" ]; then exit 1; fi
315 # FIXME: this is scary. The test could be more robust.
316 type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
317 if [ -z "$type" ]; then
318 echo "creating ${fs.fsType} filesystem on ${fs.device}..."
319 mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
320 fi
321 '';
322 unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
323 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
324 serviceConfig.Type = "oneshot";
325 };
326 in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)) // {
327 # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore.
328 # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then.
329 "mount-pstore" = {
330 serviceConfig = {
331 Type = "oneshot";
332 # skip on kernels without the pstore module
333 ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore";
334 ExecStart = pkgs.writeShellScript "mount-pstore.sh" ''
335 set -eu
336 # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons.
337 ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore
338 # wait up to 1.5 seconds for the backend to be registered and the files to appear. a systemd path unit cannot detect this happening; and succeeding after a restart would not start dependent units.
339 TRIES=15
340 while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do
341 if (( $TRIES )); then
342 sleep 0.1
343 TRIES=$((TRIES-1))
344 else
345 echo "Persistent Storage backend was not registered in time." >&2
346 break
347 fi
348 done
349 '';
350 RemainAfterExit = true;
351 };
352 unitConfig = {
353 ConditionVirtualization = "!container";
354 DefaultDependencies = false; # needed to prevent a cycle
355 };
356 before = [ "systemd-pstore.service" ];
357 wantedBy = [ "systemd-pstore.service" ];
358 };
359 };
360
361 systemd.tmpfiles.rules = [
362 "d /run/keys 0750 root ${toString config.ids.gids.keys}"
363 "z /run/keys 0750 root ${toString config.ids.gids.keys}"
364 ];
365
366 # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
367 boot.specialFileSystems = {
368 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
369 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; };
370 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; };
371 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
372 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
373
374 # To hold secrets that shouldn't be written to disk
375 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; };
376 } // optionalAttrs (!config.boot.isContainer) {
377 # systemd-nspawn populates /sys by itself, and remounting it causes all
378 # kinds of weird issues (most noticeably, waiting for host disk device
379 # nodes).
380 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; };
381 };
382
383 };
384
385}