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