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