1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}@moduleArgs:
8let
9 inherit (lib)
10 any
11 attrValues
12 concatMapStrings
13 concatMapStringsSep
14 concatStringsSep
15 elem
16 filter
17 flip
18 head
19 literalExpression
20 mkDefault
21 mkEnableOption
22 mkIf
23 mkMerge
24 mkOption
25 optional
26 optionalAttrs
27 optionalString
28 toposort
29 types
30 ;
31 inherit (utils) fsBefore;
32
33 # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
34 escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
35
36 # A list of attrnames is coerced into an attrset of bools by
37 # setting the values to true.
38 attrNamesToTrue = types.coercedTo (types.listOf types.str) (
39 enabledList: lib.genAttrs enabledList (_attrName: true)
40 ) (types.attrsOf types.bool);
41
42 addCheckDesc =
43 desc: elemType: check:
44 types.addCheck elemType check // { description = "${elemType.description} (with check: ${desc})"; };
45
46 isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null;
47 nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty;
48
49 fileSystems' = toposort fsBefore (attrValues config.fileSystems);
50
51 fileSystems =
52 if fileSystems' ? result then
53 # use topologically sorted fileSystems everywhere
54 fileSystems'.result
55 else
56 # the assertion below will catch this,
57 # but we fall back to the original order
58 # anyway so that other modules could check
59 # their assertions too
60 (attrValues config.fileSystems);
61
62 specialFSTypes = [
63 "proc"
64 "sysfs"
65 "tmpfs"
66 "ramfs"
67 "devtmpfs"
68 "devpts"
69 ];
70
71 nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str (
72 s: isNonEmpty s && (builtins.match ".+/" s) == null
73 );
74
75 coreFileSystemOpts =
76 { name, config, ... }:
77 {
78
79 options = {
80 enable = mkEnableOption "the filesystem mount" // {
81 default = true;
82 };
83
84 mountPoint = mkOption {
85 example = "/mnt/usb";
86 type = nonEmptyWithoutTrailingSlash;
87 default = name;
88 description = ''
89 Location where the file system will be mounted.
90
91 This is called `mountpoint` in {manpage}`mount(8)` and `fs_file` in {manpage}`fstab(5)`
92 '';
93 };
94
95 stratis.poolUuid = mkOption {
96 type = types.uniq (types.nullOr types.str);
97 description = ''
98 UUID of the stratis pool that the fs is located in
99
100 This is only relevant if you are using [stratis](https://stratis-storage.github.io/).
101 '';
102 example = "04c68063-90a5-4235-b9dd-6180098a20d9";
103 default = null;
104 };
105
106 device = mkOption {
107 default = null;
108 example = "/dev/sda";
109 type = types.nullOr nonEmptyStr;
110 description = ''
111 The device as passed to `mount`.
112
113 This can be any of:
114
115 - a filename of a block special device such as `/dev/sdc3`
116 - a tag such as `UUID=fdd68895-c307-4549-8c9c-90e44c71f5b7`
117 - (for bind mounts only) the source path
118 - something else depending on the {option}`fsType`. For example, `nfs` device may look like `knuth.cwi.nl:/dir`
119
120 This is called `device` in {manpage}`mount(8)` and `fs_spec` in {manpage}`fstab(5)`.
121 '';
122 };
123
124 fsType = mkOption {
125 default = "auto";
126 example = "ext3";
127 type = nonEmptyStr;
128 description = ''
129 Type of the file system.
130
131 This is the `fstype` passed to `-t` in the {manpage}`mount(8)` command, and is called `fs_vfstype` in {manpage}`fstab(5)`.
132 '';
133 };
134
135 options = mkOption {
136 default = [ "defaults" ];
137 example = [ "data=journal" ];
138 description = ''
139 Options used to mount the file system.
140
141 This is called `options` in {manpage}`mount(8)` and `fs_mntops` in {manpage}`fstab(5)`
142
143 Some options that can be used for all mounts are documented in {manpage}`mount(8)` under `FILESYSTEM-INDEPENDENT MOUNT OPTIONS`.
144
145 Options that systemd understands are documented in {manpage}`systemd.mount(5)` under `FSTAB`.
146
147 Each filesystem supports additional options, see the docs for that filesystem.
148 '';
149 type = types.nonEmptyListOf nonEmptyStr;
150 };
151
152 depends = mkOption {
153 default = [ ];
154 example = [ "/persist" ];
155 type = types.listOf nonEmptyWithoutTrailingSlash;
156 description = ''
157 List of paths that should be mounted before this one. This filesystem's
158 {option}`device` and {option}`mountPoint` are always
159 checked and do not need to be included explicitly. If a path is added
160 to this list, any other filesystem whose mount point is a parent of
161 the path will be mounted before this filesystem. The paths do not need
162 to actually be the {option}`mountPoint` of some other filesystem.
163
164 This is useful for mounts which require keys and/or configuration files residing on another filesystem.
165 '';
166 };
167
168 };
169
170 config = {
171 device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType);
172 };
173
174 };
175
176 fileSystemOpts =
177 { config, ... }:
178 {
179
180 options = {
181
182 label = mkOption {
183 default = null;
184 example = "root-partition";
185 type = types.nullOr nonEmptyStr;
186 description = ''
187 Label of the device. This simply sets {option}`device` to
188 `/dev/disk/by-id/''${label}`. Note that devices will not
189 have a label unless they contain a filesystem which
190 supports labels, such as ext4 or fat32.
191 '';
192 };
193
194 autoFormat = mkOption {
195 default = false;
196 type = types.bool;
197 description = ''
198 If the device does not currently contain a filesystem (as
199 determined by {command}`blkid`), then automatically
200 format it with the filesystem type specified in
201 {option}`fsType`. Use with caution.
202 '';
203 };
204
205 formatOptions = mkOption {
206 visible = false;
207 type = types.unspecified;
208 default = null;
209 };
210
211 autoResize = mkOption {
212 default = false;
213 type = types.bool;
214 description = ''
215 If set, the filesystem is grown to its maximum size before
216 being mounted. (This is typically the size of the containing
217 partition.) This is currently only supported for ext2/3/4
218 filesystems that are mounted during early boot.
219 '';
220 };
221
222 noCheck = mkOption {
223 default = false;
224 type = types.bool;
225 description = "Disable running fsck on this filesystem.";
226 };
227
228 };
229
230 config.device = mkIf (config.label != null) (mkDefault "/dev/disk/by-label/${escape config.label}");
231
232 config.options =
233 let
234 inInitrd = utils.fsNeededForBoot config;
235 in
236 mkMerge [
237 (mkIf config.autoResize [ "x-systemd.growfs" ])
238 (mkIf config.autoFormat [ "x-systemd.makefs" ])
239 (mkIf (utils.fsNeededForBoot config) [ "x-initrd.mount" ])
240 (mkIf
241 # With scripted stage 1, depends is implemented by sorting 'config.system.build.fileSystems'
242 (lib.length config.depends > 0 && (inInitrd -> moduleArgs.config.boot.initrd.systemd.enable))
243 (map (x: "x-systemd.requires-mounts-for=${optionalString inInitrd "/sysroot"}${x}") config.depends)
244 )
245 ];
246
247 };
248
249 # Makes sequence of `specialMount device mountPoint options fsType` commands.
250 # `systemMount` should be defined in the sourcing script.
251 makeSpecialMounts =
252 mounts:
253 pkgs.writeText "mounts.sh" (
254 concatMapStringsSep "\n" (mount: ''
255 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
256 '') mounts
257 );
258
259 makeFstabEntries =
260 let
261 fsToSkipCheck = [
262 "none"
263 "auto"
264 "overlay"
265 "iso9660"
266 "bindfs"
267 "udf"
268 "btrfs"
269 "zfs"
270 "tmpfs"
271 "bcachefs"
272 "nfs"
273 "nfs4"
274 "nilfs2"
275 "vboxsf"
276 "squashfs"
277 "glusterfs"
278 "apfs"
279 "9p"
280 "cifs"
281 "prl_fs"
282 "vmhgfs"
283 ]
284 ++ lib.optionals (!config.boot.initrd.checkJournalingFS) [
285 "ext3"
286 "ext4"
287 "reiserfs"
288 "xfs"
289 "jfs"
290 "f2fs"
291 ];
292 isBindMount = fs: builtins.elem "bind" fs.options;
293 skipCheck =
294 fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs;
295 in
296 fstabFileSystems:
297 { }:
298 concatMapStrings (
299 fs:
300 (
301 if fs.device != null then
302 escape fs.device
303 else
304 throw "No device specified for mount point ‘${fs.mountPoint}’."
305 )
306 + " "
307 + escape fs.mountPoint
308 + " "
309 + fs.fsType
310 + " "
311 + escape (builtins.concatStringsSep "," fs.options)
312 + " 0 "
313 + (
314 if skipCheck fs then
315 "0"
316 else if fs.mountPoint == "/" then
317 "1"
318 else
319 "2"
320 )
321 + "\n"
322 ) fstabFileSystems;
323
324 initrdFstab = pkgs.writeText "initrd-fstab" (
325 makeFstabEntries (filter utils.fsNeededForBoot fileSystems) { }
326 );
327
328in
329
330{
331
332 ###### interface
333
334 options = {
335
336 fileSystems = mkOption {
337 default = { };
338 example = literalExpression ''
339 {
340 "/".device = "/dev/hda1";
341 "/data" = {
342 device = "/dev/hda2";
343 fsType = "ext3";
344 options = [ "data=journal" ];
345 };
346 "/bigdisk".label = "bigdisk";
347 }
348 '';
349 type = types.attrsOf (
350 types.submodule [
351 coreFileSystemOpts
352 fileSystemOpts
353 ]
354 );
355 apply = lib.filterAttrs (_: fs: fs.enable);
356 description = ''
357 The file systems to be mounted. It must include an entry for
358 the root directory (`mountPoint = "/"`). Each
359 entry in the list is an attribute set with the following fields:
360 `mountPoint`, `device`,
361 `fsType` (a file system type recognised by
362 {command}`mount`; defaults to
363 `"auto"`), and `options`
364 (the mount options passed to {command}`mount` using the
365 {option}`-o` flag; defaults to `[ "defaults" ]`).
366
367 Instead of specifying `device`, you can also
368 specify a volume label (`label`) for file
369 systems that support it, such as ext2/ext3 (see {command}`mke2fs -L`).
370 '';
371 };
372
373 system.fsPackages = mkOption {
374 internal = true;
375 default = [ ];
376 description = "Packages supplying file system mounters and checkers.";
377 };
378
379 boot.supportedFilesystems = mkOption {
380 default = { };
381 example = literalExpression ''
382 {
383 btrfs = true;
384 zfs = lib.mkForce false;
385 }
386 '';
387 type = attrNamesToTrue;
388 description = ''
389 Names of supported filesystem types, or an attribute set of file system types
390 and their state. The set form may be used together with `lib.mkForce` to
391 explicitly disable support for specific filesystems, e.g. to disable ZFS
392 with an unsupported kernel.
393 '';
394 };
395
396 boot.specialFileSystems = mkOption {
397 default = { };
398 type = types.attrsOf (types.submodule coreFileSystemOpts);
399 apply = lib.filterAttrs (_: fs: fs.enable);
400 internal = true;
401 description = ''
402 Special filesystems that are mounted very early during boot.
403 '';
404 };
405
406 boot.devSize = mkOption {
407 default = "5%";
408 example = "32m";
409 type = types.str;
410 description = ''
411 Size limit for the /dev tmpfs. Look at {manpage}`mount(8)`, tmpfs size option,
412 for the accepted syntax.
413 '';
414 };
415
416 boot.devShmSize = mkOption {
417 default = "50%";
418 example = "256m";
419 type = types.str;
420 description = ''
421 Size limit for the /dev/shm tmpfs. Look at {manpage}`mount(8)`, tmpfs size option,
422 for the accepted syntax.
423 '';
424 };
425
426 boot.runSize = mkOption {
427 default = "25%";
428 example = "256m";
429 type = types.str;
430 description = ''
431 Size limit for the /run tmpfs. Look at {manpage}`mount(8)`, tmpfs size option,
432 for the accepted syntax.
433 '';
434 };
435 };
436
437 ###### implementation
438
439 config = {
440
441 assertions =
442 let
443 ls = sep: concatMapStringsSep sep (x: x.mountPoint);
444 resizableFSes = [
445 "ext3"
446 "ext4"
447 "btrfs"
448 "xfs"
449 ];
450 notAutoResizable = fs: fs.autoResize && !(builtins.elem fs.fsType resizableFSes);
451 in
452 [
453 {
454 assertion = !(fileSystems' ? cycle);
455 message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
456 }
457 {
458 assertion = !(any notAutoResizable fileSystems);
459 message =
460 let
461 fs = head (filter notAutoResizable fileSystems);
462 in
463 ''
464 Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = "${fs.fsType}"'
465 ${optionalString (fs.fsType == "auto") "fsType has to be explicitly set and"}
466 only the following support it: ${lib.concatStringsSep ", " resizableFSes}.
467 '';
468 }
469 {
470 assertion = !(any (fs: fs.formatOptions != null) fileSystems);
471 message = ''
472 'fileSystems.<name>.formatOptions' has been removed, since
473 systemd-makefs does not support any way to provide formatting
474 options.
475 '';
476 }
477 ]
478 ++ lib.map (fs: {
479 assertion = fs.label != null -> fs.device == "/dev/disk/by-label/${escape fs.label}";
480 message = ''
481 The filesystem with mount point ${fs.mountPoint} has its label and device set to inconsistent values:
482 label: ${toString fs.label}
483 device: ${toString fs.device}
484 'filesystems.<name>.label' and 'filesystems.<name>.device' are mutually exclusive. Please set only one.
485 '';
486 }) fileSystems;
487
488 # Export for use in other modules
489 system.build.fileSystems = fileSystems;
490 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (
491 attrValues config.boot.specialFileSystems
492 )).result;
493
494 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
495
496 # Add the mount helpers to the system path so that `mount' can find them.
497 system.fsPackages = [ pkgs.dosfstools ];
498
499 environment.systemPackages = config.system.fsPackages;
500
501 environment.etc.fstab.text =
502 let
503 swapOptions =
504 sw:
505 concatStringsSep "," (
506 sw.options
507 ++ optional (sw.priority != null) "pri=${toString sw.priority}"
508 ++
509 optional (sw.discardPolicy != null)
510 "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}"
511 );
512 in
513 ''
514 # This is a generated file. Do not edit!
515 #
516 # To make changes, edit the fileSystems and swapDevices NixOS options
517 # in your /etc/nixos/configuration.nix file.
518 #
519 # <file system> <mount point> <type> <options> <dump> <pass>
520
521 # Filesystems.
522 ${makeFstabEntries fileSystems { }}
523
524 ${optionalString (config.swapDevices != [ ]) "# Swap devices."}
525 ${flip concatMapStrings config.swapDevices (sw: "${sw.realDevice} none swap ${swapOptions sw}\n")}
526 '';
527
528 boot.initrd.systemd.storePaths = [ initrdFstab ];
529 boot.initrd.systemd.managerEnvironment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
530 boot.initrd.systemd.services.initrd-parse-etc.environment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
531
532 # Provide a target that pulls in all filesystems.
533 systemd.targets.fs = {
534 description = "All File Systems";
535 wants = [
536 "local-fs.target"
537 "remote-fs.target"
538 ];
539 };
540
541 systemd.tmpfiles.rules = [
542 "d /run/keys 0750 root ${toString config.ids.gids.keys}"
543 "z /run/keys 0750 root ${toString config.ids.gids.keys}"
544 ];
545
546 # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
547 boot.specialFileSystems = {
548 "/proc" = {
549 fsType = "proc";
550 options = [
551 "nosuid"
552 "noexec"
553 "nodev"
554 ];
555 };
556 "/run" = {
557 fsType = "tmpfs";
558 options = [
559 "nosuid"
560 "nodev"
561 "strictatime"
562 "mode=755"
563 "size=${config.boot.runSize}"
564 ];
565 };
566 "/dev" = {
567 fsType = "devtmpfs";
568 options = [
569 "nosuid"
570 "strictatime"
571 "mode=755"
572 "size=${config.boot.devSize}"
573 ];
574 };
575 "/dev/shm" = {
576 fsType = "tmpfs";
577 options = [
578 "nosuid"
579 "nodev"
580 "strictatime"
581 "mode=1777"
582 "size=${config.boot.devShmSize}"
583 ];
584 };
585 "/dev/pts" = {
586 fsType = "devpts";
587 options = [
588 "nosuid"
589 "noexec"
590 "mode=620"
591 "ptmxmode=0666"
592 "gid=${toString config.ids.gids.tty}"
593 ];
594 };
595
596 # To hold secrets that shouldn't be written to disk
597 "/run/keys" = {
598 fsType = "ramfs";
599 options = [
600 "nosuid"
601 "nodev"
602 "mode=750"
603 ];
604 };
605 }
606 // optionalAttrs (!config.boot.isContainer) {
607 # systemd-nspawn populates /sys by itself, and remounting it causes all
608 # kinds of weird issues (most noticeably, waiting for host disk device
609 # nodes).
610 "/sys" = {
611 fsType = "sysfs";
612 options = [
613 "nosuid"
614 "noexec"
615 "nodev"
616 ];
617 };
618 };
619
620 };
621
622}