1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 udev = config.systemd.package;
8
9 cfg = config.services.udev;
10
11 initrdUdevRules = pkgs.runCommand "initrd-udev-rules" {} ''
12 mkdir -p $out/etc/udev/rules.d
13 for f in 60-cdrom_id 60-persistent-storage 75-net-description 80-drivers 80-net-setup-link; do
14 ln -s ${config.boot.initrd.systemd.package}/lib/udev/rules.d/$f.rules $out/etc/udev/rules.d
15 done
16 '';
17
18
19 extraUdevRules = pkgs.writeTextFile {
20 name = "extra-udev-rules";
21 text = cfg.extraRules;
22 destination = "/etc/udev/rules.d/99-local.rules";
23 };
24
25 extraHwdbFile = pkgs.writeTextFile {
26 name = "extra-hwdb-file";
27 text = cfg.extraHwdb;
28 destination = "/etc/udev/hwdb.d/99-local.hwdb";
29 };
30
31 nixosRules = ''
32 # Miscellaneous devices.
33 KERNEL=="kvm", MODE="0666"
34
35 # Needed for gpm.
36 SUBSYSTEM=="input", KERNEL=="mice", TAG+="systemd"
37 '';
38
39 nixosInitrdRules = ''
40 # Mark dm devices as db_persist so that they are kept active after switching root
41 SUBSYSTEM=="block", KERNEL=="dm-[0-9]*", ACTION=="add|change", OPTIONS+="db_persist"
42 '';
43
44 # Perform substitutions in all udev rules files.
45 udevRulesFor = { name, udevPackages, udevPath, udev, systemd, binPackages, initrdBin ? null }: pkgs.runCommand name
46 { preferLocalBuild = true;
47 allowSubstitutes = false;
48 packages = unique (map toString udevPackages);
49 }
50 ''
51 mkdir -p $out
52 shopt -s nullglob
53 set +o pipefail
54
55 # Set a reasonable $PATH for programs called by udev rules.
56 echo 'ENV{PATH}="${udevPath}/bin:${udevPath}/sbin"' > $out/00-path.rules
57
58 # Add the udev rules from other packages.
59 for i in $packages; do
60 echo "Adding rules for package $i"
61 for j in $i/{etc,lib}/udev/rules.d/*; do
62 echo "Copying $j to $out/$(basename $j)"
63 cat $j > $out/$(basename $j)
64 done
65 done
66
67 # Fix some paths in the standard udev rules. Hacky.
68 for i in $out/*.rules; do
69 substituteInPlace $i \
70 --replace \"/sbin/modprobe \"${pkgs.kmod}/bin/modprobe \
71 --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
72 --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
73 --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
74 --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
75 --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
76 ${optionalString (initrdBin != null) ''
77 substituteInPlace $i --replace '/run/current-system/systemd' "${removeSuffix "/bin" initrdBin}"
78 ''}
79 done
80
81 echo -n "Checking that all programs called by relative paths in udev rules exist in ${udev}/lib/udev... "
82 import_progs=$(grep 'IMPORT{program}="[^/$]' $out/* |
83 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
84 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="[^/$]' |
85 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
86 for i in $import_progs $run_progs; do
87 if [[ ! -x ${udev}/lib/udev/$i && ! $i =~ socket:.* ]]; then
88 echo "FAIL"
89 echo "$i is called in udev rules but not installed by udev"
90 exit 1
91 fi
92 done
93 echo "OK"
94
95 echo -n "Checking that all programs called by absolute paths in udev rules exist... "
96 import_progs=$(grep 'IMPORT{program}="\/' $out/* |
97 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
98 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="/' |
99 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
100 for i in $import_progs $run_progs; do
101 # if the path refers to /run/current-system/systemd, replace with config.systemd.package
102 if [[ $i == /run/current-system/systemd* ]]; then
103 i="${systemd}/''${i#/run/current-system/systemd/}"
104 fi
105
106 if [[ ! -x $i ]]; then
107 echo "FAIL"
108 echo "$i is called in udev rules but is not executable or does not exist"
109 exit 1
110 fi
111 done
112 echo "OK"
113
114 filesToFixup="$(for i in "$out"/*; do
115 grep -l '\B\(/usr\)\?/s\?bin' "$i" || :
116 done)"
117
118 if [ -n "$filesToFixup" ]; then
119 echo "Consider fixing the following udev rules:"
120 echo "$filesToFixup" | while read localFile; do
121 remoteFile="origin unknown"
122 for i in ${toString binPackages}; do
123 for j in "$i"/*/udev/rules.d/*; do
124 [ -e "$out/$(basename "$j")" ] || continue
125 [ "$(basename "$j")" = "$(basename "$localFile")" ] || continue
126 remoteFile="originally from $j"
127 break 2
128 done
129 done
130 refs="$(
131 grep -o '\B\(/usr\)\?/s\?bin/[^ "]\+' "$localFile" \
132 | sed -e ':r;N;''${s/\n/ and /;br};s/\n/, /g;br'
133 )"
134 echo "$localFile ($remoteFile) contains references to $refs."
135 done
136 exit 1
137 fi
138
139 # If auto-configuration is disabled, then remove
140 # udev's 80-drivers.rules file, which contains rules for
141 # automatically calling modprobe.
142 ${optionalString (!config.boot.hardwareScan) ''
143 ln -s /dev/null $out/80-drivers.rules
144 ''}
145 '';
146
147 hwdbBin = pkgs.runCommand "hwdb.bin"
148 { preferLocalBuild = true;
149 allowSubstitutes = false;
150 packages = unique (map toString ([udev] ++ cfg.packages));
151 }
152 ''
153 mkdir -p etc/udev/hwdb.d
154 for i in $packages; do
155 echo "Adding hwdb files for package $i"
156 for j in $i/{etc,lib}/udev/hwdb.d/*; do
157 ln -s $j etc/udev/hwdb.d/$(basename $j)
158 done
159 done
160
161 echo "Generating hwdb database..."
162 # hwdb --update doesn't return error code even on errors!
163 res="$(${pkgs.buildPackages.systemd}/bin/systemd-hwdb --root=$(pwd) update 2>&1)"
164 echo "$res"
165 [ -z "$(echo "$res" | egrep '^Error')" ]
166 mv etc/udev/hwdb.bin $out
167 '';
168
169 compressFirmware = firmware: if (config.boot.kernelPackages.kernelAtLeast "5.3" && (firmware.compressFirmware or true)) then
170 pkgs.compressFirmwareXz firmware
171 else
172 id firmware;
173
174 # Udev has a 512-character limit for ENV{PATH}, so create a symlink
175 # tree to work around this.
176 udevPath = pkgs.buildEnv {
177 name = "udev-path";
178 paths = cfg.path;
179 pathsToLink = [ "/bin" "/sbin" ];
180 ignoreCollisions = true;
181 };
182
183in
184
185{
186
187 ###### interface
188
189 options = {
190 boot.hardwareScan = mkOption {
191 type = types.bool;
192 default = true;
193 description = lib.mdDoc ''
194 Whether to try to load kernel modules for all detected hardware.
195 Usually this does a good job of providing you with the modules
196 you need, but sometimes it can crash the system or cause other
197 nasty effects.
198 '';
199 };
200
201 services.udev = {
202 enable = mkEnableOption (lib.mdDoc "udev") // {
203 default = true;
204 };
205
206 packages = mkOption {
207 type = types.listOf types.path;
208 default = [];
209 description = lib.mdDoc ''
210 List of packages containing {command}`udev` rules.
211 All files found in
212 {file}`«pkg»/etc/udev/rules.d` and
213 {file}`«pkg»/lib/udev/rules.d`
214 will be included.
215 '';
216 apply = map getBin;
217 };
218
219 path = mkOption {
220 type = types.listOf types.path;
221 default = [];
222 description = lib.mdDoc ''
223 Packages added to the {env}`PATH` environment variable when
224 executing programs from Udev rules.
225 '';
226 };
227
228 extraRules = mkOption {
229 default = "";
230 example = ''
231 ENV{ID_VENDOR_ID}=="046d", ENV{ID_MODEL_ID}=="0825", ENV{PULSE_IGNORE}="1"
232 '';
233 type = types.lines;
234 description = lib.mdDoc ''
235 Additional {command}`udev` rules. They'll be written
236 into file {file}`99-local.rules`. Thus they are
237 read and applied after all other rules.
238 '';
239 };
240
241 extraHwdb = mkOption {
242 default = "";
243 example = ''
244 evdev:input:b0003v05AFp8277*
245 KEYBOARD_KEY_70039=leftalt
246 KEYBOARD_KEY_700e2=leftctrl
247 '';
248 type = types.lines;
249 description = lib.mdDoc ''
250 Additional {command}`hwdb` files. They'll be written
251 into file {file}`99-local.hwdb`. Thus they are
252 read after all other files.
253 '';
254 };
255
256 };
257
258 hardware.firmware = mkOption {
259 type = types.listOf types.package;
260 default = [];
261 description = lib.mdDoc ''
262 List of packages containing firmware files. Such files
263 will be loaded automatically if the kernel asks for them
264 (i.e., when it has detected specific hardware that requires
265 firmware to function). If multiple packages contain firmware
266 files with the same name, the first package in the list takes
267 precedence. Note that you must rebuild your system if you add
268 files to any of these directories.
269 '';
270 apply = list: pkgs.buildEnv {
271 name = "firmware";
272 paths = map compressFirmware list;
273 pathsToLink = [ "/lib/firmware" ];
274 ignoreCollisions = true;
275 };
276 };
277
278 networking.usePredictableInterfaceNames = mkOption {
279 default = true;
280 type = types.bool;
281 description = lib.mdDoc ''
282 Whether to assign [predictable names to network interfaces](http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames).
283 If enabled, interfaces
284 are assigned names that contain topology information
285 (e.g. `wlp3s0`) and thus should be stable
286 across reboots. If disabled, names depend on the order in
287 which interfaces are discovered by the kernel, which may
288 change randomly across reboots; for instance, you may find
289 `eth0` and `eth1` flipping
290 unpredictably.
291 '';
292 };
293
294 boot.initrd.services.udev = {
295
296 packages = mkOption {
297 type = types.listOf types.path;
298 default = [];
299 visible = false;
300 description = lib.mdDoc ''
301 *This will only be used when systemd is used in stage 1.*
302
303 List of packages containing {command}`udev` rules that will be copied to stage 1.
304 All files found in
305 {file}`«pkg»/etc/udev/rules.d` and
306 {file}`«pkg»/lib/udev/rules.d`
307 will be included.
308 '';
309 };
310
311 binPackages = mkOption {
312 type = types.listOf types.path;
313 default = [];
314 visible = false;
315 description = lib.mdDoc ''
316 *This will only be used when systemd is used in stage 1.*
317
318 Packages to search for binaries that are referenced by the udev rules in stage 1.
319 This list always contains /bin of the initrd.
320 '';
321 apply = map getBin;
322 };
323
324 rules = mkOption {
325 default = "";
326 example = ''
327 SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1D:60:B9:6D:4F", KERNEL=="eth*", NAME="my_fast_network_card"
328 '';
329 type = types.lines;
330 description = lib.mdDoc ''
331 {command}`udev` rules to include in the initrd
332 *only*. They'll be written into file
333 {file}`99-local.rules`. Thus they are read and applied
334 after the essential initrd rules.
335 '';
336 };
337
338 };
339
340 };
341
342
343 ###### implementation
344
345 config = mkIf cfg.enable {
346
347 services.udev.extraRules = nixosRules;
348
349 services.udev.packages = [ extraUdevRules extraHwdbFile ];
350
351 services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.util-linux udev ];
352
353 boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
354
355 boot.initrd.extraUdevRulesCommands = optionalString (!config.boot.initrd.systemd.enable && config.boot.initrd.services.udev.rules != "")
356 ''
357 cat <<'EOF' > $out/99-local.rules
358 ${config.boot.initrd.services.udev.rules}
359 EOF
360 '';
361
362 boot.initrd.services.udev.rules = nixosInitrdRules;
363
364 boot.initrd.systemd.additionalUpstreamUnits = [
365 "initrd-udevadm-cleanup-db.service"
366 "systemd-udevd-control.socket"
367 "systemd-udevd-kernel.socket"
368 "systemd-udevd.service"
369 "systemd-udev-settle.service"
370 "systemd-udev-trigger.service"
371 ];
372 boot.initrd.systemd.storePaths = [
373 "${config.boot.initrd.systemd.package}/lib/systemd/systemd-udevd"
374 "${config.boot.initrd.systemd.package}/lib/udev/ata_id"
375 "${config.boot.initrd.systemd.package}/lib/udev/cdrom_id"
376 "${config.boot.initrd.systemd.package}/lib/udev/scsi_id"
377 "${config.boot.initrd.systemd.package}/lib/udev/rules.d"
378 ] ++ map (x: "${x}/bin") config.boot.initrd.services.udev.binPackages;
379
380 # Generate the udev rules for the initrd
381 boot.initrd.systemd.contents = {
382 "/etc/udev/rules.d".source = udevRulesFor {
383 name = "initrd-udev-rules";
384 initrdBin = config.boot.initrd.systemd.contents."/bin".source;
385 udevPackages = config.boot.initrd.services.udev.packages;
386 udevPath = config.boot.initrd.systemd.contents."/bin".source;
387 udev = config.boot.initrd.systemd.package;
388 systemd = config.boot.initrd.systemd.package;
389 binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ];
390 };
391 };
392 # Insert initrd rules
393 boot.initrd.services.udev.packages = [
394 initrdUdevRules
395 (mkIf (config.boot.initrd.services.udev.rules != "") (pkgs.writeTextFile {
396 name = "initrd-udev-rules";
397 destination = "/etc/udev/rules.d/99-local.rules";
398 text = config.boot.initrd.services.udev.rules;
399 }))
400 ];
401
402 environment.etc =
403 {
404 "udev/rules.d".source = udevRulesFor {
405 name = "udev-rules";
406 udevPackages = cfg.packages;
407 systemd = config.systemd.package;
408 binPackages = cfg.packages;
409 inherit udevPath udev;
410 };
411 "udev/hwdb.bin".source = hwdbBin;
412 };
413
414 system.requiredKernelConfig = with config.lib.kernelConfig; [
415 (isEnabled "UNIX")
416 (isYes "INOTIFY_USER")
417 (isYes "NET")
418 ];
419
420 # We don't place this into `extraModprobeConfig` so that stage-1 ramdisk doesn't bloat.
421 environment.etc."modprobe.d/firmware.conf".text = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
422
423 system.activationScripts.udevd =
424 ''
425 # The deprecated hotplug uevent helper is not used anymore
426 if [ -e /proc/sys/kernel/hotplug ]; then
427 echo "" > /proc/sys/kernel/hotplug
428 fi
429
430 # Allow the kernel to find our firmware.
431 if [ -e /sys/module/firmware_class/parameters/path ]; then
432 echo -n "${config.hardware.firmware}/lib/firmware" > /sys/module/firmware_class/parameters/path
433 fi
434 '';
435
436 systemd.services.systemd-udevd =
437 { restartTriggers = cfg.packages;
438 };
439
440 };
441
442 imports = [
443 (mkRenamedOptionModule [ "services" "udev" "initrdRules" ] [ "boot" "initrd" "services" "udev" "rules" ])
444 ];
445}