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 nonEmptyStr = addCheckDesc "non-empty" types.str
11 (x: x != "" && ! (all (c: c == " " || c == "\t") (stringToCharacters x)));
12
13 fileSystems' = toposort fsBefore (attrValues config.fileSystems);
14
15 fileSystems = if fileSystems' ? "result"
16 then # use topologically sorted fileSystems everywhere
17 fileSystems'.result
18 else # the assertion below will catch this,
19 # but we fall back to the original order
20 # anyway so that other modules could check
21 # their assertions too
22 (attrValues config.fileSystems);
23
24 prioOption = prio: optionalString (prio != null) " pri=${toString prio}";
25
26 specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ];
27
28 coreFileSystemOpts = { name, config, ... }: {
29
30 options = {
31
32 mountPoint = mkOption {
33 example = "/mnt/usb";
34 type = nonEmptyStr;
35 description = "Location of the mounted the file system.";
36 };
37
38 device = mkOption {
39 default = null;
40 example = "/dev/sda";
41 type = types.nullOr nonEmptyStr;
42 description = "Location of the device.";
43 };
44
45 fsType = mkOption {
46 default = "auto";
47 example = "ext3";
48 type = nonEmptyStr;
49 description = "Type of the file system.";
50 };
51
52 options = mkOption {
53 default = [ "defaults" ];
54 example = [ "data=journal" ];
55 description = "Options used to mount the file system.";
56 type = types.listOf nonEmptyStr;
57 };
58
59 };
60
61 config = {
62 mountPoint = mkDefault name;
63 device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType);
64 };
65
66 };
67
68 fileSystemOpts = { config, ... }: {
69
70 options = {
71
72 label = mkOption {
73 default = null;
74 example = "root-partition";
75 type = types.nullOr nonEmptyStr;
76 description = "Label of the device (if any).";
77 };
78
79 autoFormat = mkOption {
80 default = false;
81 type = types.bool;
82 description = ''
83 If the device does not currently contain a filesystem (as
84 determined by <command>blkid</command>, then automatically
85 format it with the filesystem type specified in
86 <option>fsType</option>. Use with caution.
87 '';
88 };
89
90 formatOptions = mkOption {
91 default = "";
92 type = types.str;
93 description = ''
94 If <option>autoFormat</option> option is set specifies
95 extra options passed to mkfs.
96 '';
97 };
98
99 autoResize = mkOption {
100 default = false;
101 type = types.bool;
102 description = ''
103 If set, the filesystem is grown to its maximum size before
104 being mounted. (This is typically the size of the containing
105 partition.) This is currently only supported for ext2/3/4
106 filesystems that are mounted during early boot.
107 '';
108 };
109
110 noCheck = mkOption {
111 default = false;
112 type = types.bool;
113 description = "Disable running fsck on this filesystem.";
114 };
115
116 };
117
118 config = {
119 options = mkIf config.autoResize [ "x-nixos.autoresize" ];
120
121 # -F needed to allow bare block device without partitions
122 formatOptions = mkIf ((builtins.substring 0 3 config.fsType) == "ext") (mkDefault "-F");
123 };
124
125 };
126
127 # Makes sequence of `specialMount device mountPoint options fsType` commands.
128 # `systemMount` should be defined in the sourcing script.
129 makeSpecialMounts = mounts:
130 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: ''
131 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
132 '') mounts);
133
134in
135
136{
137
138 ###### interface
139
140 options = {
141
142 fileSystems = mkOption {
143 default = {};
144 example = literalExample ''
145 {
146 "/".device = "/dev/hda1";
147 "/data" = {
148 device = "/dev/hda2";
149 fsType = "ext3";
150 options = [ "data=journal" ];
151 };
152 "/bigdisk".label = "bigdisk";
153 }
154 '';
155 type = types.loaOf (types.submodule [coreFileSystemOpts fileSystemOpts]);
156 description = ''
157 The file systems to be mounted. It must include an entry for
158 the root directory (<literal>mountPoint = "/"</literal>). Each
159 entry in the list is an attribute set with the following fields:
160 <literal>mountPoint</literal>, <literal>device</literal>,
161 <literal>fsType</literal> (a file system type recognised by
162 <command>mount</command>; defaults to
163 <literal>"auto"</literal>), and <literal>options</literal>
164 (the mount options passed to <command>mount</command> using the
165 <option>-o</option> flag; defaults to <literal>[ "defaults" ]</literal>).
166
167 Instead of specifying <literal>device</literal>, you can also
168 specify a volume label (<literal>label</literal>) for file
169 systems that support it, such as ext2/ext3 (see <command>mke2fs
170 -L</command>).
171 '';
172 };
173
174 system.fsPackages = mkOption {
175 internal = true;
176 default = [ ];
177 description = "Packages supplying file system mounters and checkers.";
178 };
179
180 boot.supportedFilesystems = mkOption {
181 default = [ ];
182 example = [ "btrfs" ];
183 type = types.listOf types.str;
184 description = "Names of supported filesystem types.";
185 };
186
187 boot.specialFileSystems = mkOption {
188 default = {};
189 type = types.loaOf (types.submodule coreFileSystemOpts);
190 internal = true;
191 description = ''
192 Special filesystems that are mounted very early during boot.
193 '';
194 };
195
196 };
197
198
199 ###### implementation
200
201 config = {
202
203 assertions = let
204 ls = sep: concatMapStringsSep sep (x: x.mountPoint);
205 in [
206 { assertion = ! (fileSystems' ? "cycle");
207 message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
208 }
209 ];
210
211 # Export for use in other modules
212 system.build.fileSystems = fileSystems;
213 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result;
214
215 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
216
217 # Add the mount helpers to the system path so that `mount' can find them.
218 system.fsPackages = [ pkgs.dosfstools ];
219
220 environment.systemPackages = [ pkgs.fuse ] ++ config.system.fsPackages;
221
222 environment.etc.fstab.text =
223 let
224 fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" ];
225 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
226 in ''
227 # This is a generated file. Do not edit!
228 #
229 # To make changes, edit the fileSystems and swapDevices NixOS options
230 # in your /etc/nixos/configuration.nix file.
231
232 # Filesystems.
233 ${concatMapStrings (fs:
234 (if fs.device != null then fs.device
235 else if fs.label != null then "/dev/disk/by-label/${fs.label}"
236 else throw "No device specified for mount point ‘${fs.mountPoint}’.")
237 + " " + fs.mountPoint
238 + " " + fs.fsType
239 + " " + builtins.concatStringsSep "," fs.options
240 + " 0"
241 + " " + (if skipCheck fs then "0" else
242 if fs.mountPoint == "/" then "1" else "2")
243 + "\n"
244 ) fileSystems}
245
246 # Swap devices.
247 ${flip concatMapStrings config.swapDevices (sw:
248 "${sw.realDevice} none swap${prioOption sw.priority}\n"
249 )}
250 '';
251
252 # Provide a target that pulls in all filesystems.
253 systemd.targets.fs =
254 { description = "All File Systems";
255 wants = [ "local-fs.target" "remote-fs.target" ];
256 };
257
258 # Emit systemd services to format requested filesystems.
259 systemd.services =
260 let
261
262 formatDevice = fs:
263 let
264 mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
265 device' = escapeSystemdPath fs.device;
266 device'' = "${device'}.device";
267 in nameValuePair "mkfs-${device'}"
268 { description = "Initialisation of Filesystem ${fs.device}";
269 wantedBy = [ mountPoint' ];
270 before = [ mountPoint' "systemd-fsck@${device'}.service" ];
271 requires = [ device'' ];
272 after = [ device'' ];
273 path = [ pkgs.utillinux ] ++ config.system.fsPackages;
274 script =
275 ''
276 if ! [ -e "${fs.device}" ]; then exit 1; fi
277 # FIXME: this is scary. The test could be more robust.
278 type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
279 if [ -z "$type" ]; then
280 echo "creating ${fs.fsType} filesystem on ${fs.device}..."
281 mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
282 fi
283 '';
284 unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
285 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
286 serviceConfig.Type = "oneshot";
287 };
288
289 in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems));
290
291 # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
292 boot.specialFileSystems = {
293 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
294 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; };
295 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; };
296 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
297 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
298
299 # To hold secrets that shouldn't be written to disk (generally used for NixOps, harmless elsewhere)
300 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" "gid=${toString config.ids.gids.keys}" ]; };
301 } // optionalAttrs (!config.boot.isContainer) {
302 # systemd-nspawn populates /sys by itself, and remounting it causes all
303 # kinds of weird issues (most noticeably, waiting for host disk device
304 # nodes).
305 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; };
306 };
307
308 };
309
310}