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 = lib.mdDoc "Location of the mounted file system.";
37 };
38
39 device = mkOption {
40 default = null;
41 example = "/dev/sda";
42 type = types.nullOr nonEmptyStr;
43 description = lib.mdDoc "Location of the device.";
44 };
45
46 fsType = mkOption {
47 default = "auto";
48 example = "ext3";
49 type = nonEmptyStr;
50 description = lib.mdDoc "Type of the file system.";
51 };
52
53 options = mkOption {
54 default = [ "defaults" ];
55 example = [ "data=journal" ];
56 description = lib.mdDoc "Options used to mount the file system.";
57 type = types.nonEmptyListOf nonEmptyStr;
58 };
59
60 depends = mkOption {
61 default = [ ];
62 example = [ "/persist" ];
63 type = types.listOf nonEmptyWithoutTrailingSlash;
64 description = lib.mdDoc ''
65 List of paths that should be mounted before this one. This filesystem's
66 {option}`device` and {option}`mountPoint` 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` 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 = lib.mdDoc "Label of the device (if any).";
92 };
93
94 autoFormat = mkOption {
95 default = false;
96 type = types.bool;
97 description = lib.mdDoc ''
98 If the device does not currently contain a filesystem (as
99 determined by {command}`blkid`, then automatically
100 format it with the filesystem type specified in
101 {option}`fsType`. Use with caution.
102 '';
103 };
104
105 formatOptions = mkOption {
106 default = "";
107 type = types.str;
108 description = lib.mdDoc ''
109 If {option}`autoFormat` 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 = lib.mdDoc ''
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 = lib.mdDoc "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 = mkMerge [
144 (mkIf config.autoResize [ "x-nixos.autoresize" ])
145 (mkIf (utils.fsNeededForBoot config) [ "x-initrd.mount" ])
146 ];
147 formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
148 };
149
150 };
151
152 # Makes sequence of `specialMount device mountPoint options fsType` commands.
153 # `systemMount` should be defined in the sourcing script.
154 makeSpecialMounts = mounts:
155 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: ''
156 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
157 '') mounts);
158
159 makeFstabEntries =
160 let
161 fsToSkipCheck = [
162 "none"
163 "auto"
164 "overlay"
165 "iso9660"
166 "bindfs"
167 "udf"
168 "btrfs"
169 "zfs"
170 "tmpfs"
171 "bcachefs"
172 "nfs"
173 "nfs4"
174 "nilfs2"
175 "vboxsf"
176 "squashfs"
177 "glusterfs"
178 "apfs"
179 "9p"
180 "cifs"
181 "prl_fs"
182 "vmhgfs"
183 ] ++ lib.optionals (!config.boot.initrd.checkJournalingFS) [
184 "ext3"
185 "ext4"
186 "reiserfs"
187 "xfs"
188 "jfs"
189 "f2fs"
190 ];
191 isBindMount = fs: builtins.elem "bind" fs.options;
192 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs;
193 # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
194 escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
195 in fstabFileSystems: { rootPrefix ? "", extraOpts ? (fs: []) }: concatMapStrings (fs:
196 (optionalString (isBindMount fs) (escape rootPrefix))
197 + (if fs.device != null then escape fs.device
198 else if fs.label != null then "/dev/disk/by-label/${escape fs.label}"
199 else throw "No device specified for mount point ‘${fs.mountPoint}’.")
200 + " " + escape fs.mountPoint
201 + " " + fs.fsType
202 + " " + escape (builtins.concatStringsSep "," (fs.options ++ (extraOpts fs)))
203 + " 0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2")
204 + "\n"
205 ) fstabFileSystems;
206
207 initrdFstab = pkgs.writeText "initrd-fstab" (makeFstabEntries (filter utils.fsNeededForBoot fileSystems) {
208 rootPrefix = "/sysroot";
209 extraOpts = fs:
210 (optional fs.autoResize "x-systemd.growfs")
211 ++ (optional fs.autoFormat "x-systemd.makefs");
212 });
213
214in
215
216{
217
218 ###### interface
219
220 options = {
221
222 fileSystems = mkOption {
223 default = {};
224 example = literalExpression ''
225 {
226 "/".device = "/dev/hda1";
227 "/data" = {
228 device = "/dev/hda2";
229 fsType = "ext3";
230 options = [ "data=journal" ];
231 };
232 "/bigdisk".label = "bigdisk";
233 }
234 '';
235 type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]);
236 description = lib.mdDoc ''
237 The file systems to be mounted. It must include an entry for
238 the root directory (`mountPoint = "/"`). Each
239 entry in the list is an attribute set with the following fields:
240 `mountPoint`, `device`,
241 `fsType` (a file system type recognised by
242 {command}`mount`; defaults to
243 `"auto"`), and `options`
244 (the mount options passed to {command}`mount` using the
245 {option}`-o` flag; defaults to `[ "defaults" ]`).
246
247 Instead of specifying `device`, you can also
248 specify a volume label (`label`) for file
249 systems that support it, such as ext2/ext3 (see {command}`mke2fs -L`).
250 '';
251 };
252
253 system.fsPackages = mkOption {
254 internal = true;
255 default = [ ];
256 description = lib.mdDoc "Packages supplying file system mounters and checkers.";
257 };
258
259 boot.supportedFilesystems = mkOption {
260 default = [ ];
261 example = [ "btrfs" ];
262 type = types.listOf types.str;
263 description = lib.mdDoc "Names of supported filesystem types.";
264 };
265
266 boot.specialFileSystems = mkOption {
267 default = {};
268 type = types.attrsOf (types.submodule coreFileSystemOpts);
269 internal = true;
270 description = lib.mdDoc ''
271 Special filesystems that are mounted very early during boot.
272 '';
273 };
274
275 boot.devSize = mkOption {
276 default = "5%";
277 example = "32m";
278 type = types.str;
279 description = lib.mdDoc ''
280 Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option,
281 for the accepted syntax.
282 '';
283 };
284
285 boot.devShmSize = mkOption {
286 default = "50%";
287 example = "256m";
288 type = types.str;
289 description = lib.mdDoc ''
290 Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option,
291 for the accepted syntax.
292 '';
293 };
294
295 boot.runSize = mkOption {
296 default = "25%";
297 example = "256m";
298 type = types.str;
299 description = lib.mdDoc ''
300 Size limit for the /run tmpfs. Look at mount(8), tmpfs size option,
301 for the accepted syntax.
302 '';
303 };
304 };
305
306
307 ###### implementation
308
309 config = {
310
311 assertions = let
312 ls = sep: concatMapStringsSep sep (x: x.mountPoint);
313 notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs");
314 in [
315 { assertion = ! (fileSystems' ? cycle);
316 message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
317 }
318 { assertion = ! (any notAutoResizable fileSystems);
319 message = let
320 fs = head (filter notAutoResizable fileSystems);
321 in
322 "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${optionalString (fs.fsType == "auto") " fsType has to be explicitly set and"} only the ext filesystems and f2fs support it.";
323 }
324 ];
325
326 # Export for use in other modules
327 system.build.fileSystems = fileSystems;
328 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result;
329
330 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
331
332 # Add the mount helpers to the system path so that `mount' can find them.
333 system.fsPackages = [ pkgs.dosfstools ];
334
335 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages;
336
337 environment.etc.fstab.text =
338 let
339 swapOptions = sw: concatStringsSep "," (
340 sw.options
341 ++ optional (sw.priority != null) "pri=${toString sw.priority}"
342 ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}"
343 );
344 in ''
345 # This is a generated file. Do not edit!
346 #
347 # To make changes, edit the fileSystems and swapDevices NixOS options
348 # in your /etc/nixos/configuration.nix file.
349 #
350 # <file system> <mount point> <type> <options> <dump> <pass>
351
352 # Filesystems.
353 ${makeFstabEntries fileSystems {}}
354
355 # Swap devices.
356 ${flip concatMapStrings config.swapDevices (sw:
357 "${sw.realDevice} none swap ${swapOptions sw}\n"
358 )}
359 '';
360
361 boot.initrd.systemd.storePaths = [initrdFstab];
362 boot.initrd.systemd.managerEnvironment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
363 boot.initrd.systemd.services.initrd-parse-etc.environment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
364
365 # Provide a target that pulls in all filesystems.
366 systemd.targets.fs =
367 { description = "All File Systems";
368 wants = [ "local-fs.target" "remote-fs.target" ];
369 };
370
371 systemd.services =
372
373 # Emit systemd services to format requested filesystems.
374 let
375 formatDevice = fs:
376 let
377 mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
378 device' = escapeSystemdPath fs.device;
379 device'' = "${device'}.device";
380 in nameValuePair "mkfs-${device'}"
381 { description = "Initialisation of Filesystem ${fs.device}";
382 wantedBy = [ mountPoint' ];
383 before = [ mountPoint' "systemd-fsck@${device'}.service" ];
384 requires = [ device'' ];
385 after = [ device'' ];
386 path = [ pkgs.util-linux ] ++ config.system.fsPackages;
387 script =
388 ''
389 if ! [ -e "${fs.device}" ]; then exit 1; fi
390 # FIXME: this is scary. The test could be more robust.
391 type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
392 if [ -z "$type" ]; then
393 echo "creating ${fs.fsType} filesystem on ${fs.device}..."
394 mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
395 fi
396 '';
397 unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
398 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
399 serviceConfig.Type = "oneshot";
400 };
401 in listToAttrs (map formatDevice (filter (fs: fs.autoFormat && !(utils.fsNeededForBoot fs)) fileSystems)) // {
402 # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore.
403 # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then.
404 "mount-pstore" = {
405 serviceConfig = {
406 Type = "oneshot";
407 # skip on kernels without the pstore module
408 ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore";
409 ExecStart = pkgs.writeShellScript "mount-pstore.sh" ''
410 set -eu
411 # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons.
412 ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore
413 # 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.
414 TRIES=15
415 while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do
416 if (( $TRIES )); then
417 sleep 0.1
418 TRIES=$((TRIES-1))
419 else
420 echo "Persistent Storage backend was not registered in time." >&2
421 break
422 fi
423 done
424 '';
425 RemainAfterExit = true;
426 };
427 unitConfig = {
428 ConditionVirtualization = "!container";
429 DefaultDependencies = false; # needed to prevent a cycle
430 };
431 before = [ "systemd-pstore.service" ];
432 wantedBy = [ "systemd-pstore.service" ];
433 };
434 };
435
436 systemd.tmpfiles.rules = [
437 "d /run/keys 0750 root ${toString config.ids.gids.keys}"
438 "z /run/keys 0750 root ${toString config.ids.gids.keys}"
439 ];
440
441 # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
442 boot.specialFileSystems = {
443 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
444 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; };
445 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; };
446 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
447 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
448
449 # To hold secrets that shouldn't be written to disk
450 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; };
451 } // optionalAttrs (!config.boot.isContainer) {
452 # systemd-nspawn populates /sys by itself, and remounting it causes all
453 # kinds of weird issues (most noticeably, waiting for host disk device
454 # nodes).
455 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; };
456 };
457
458 };
459
460}