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