1 { config, lib, pkgs, ... }:
7 udev = config.systemd.package;
9 cfg = config.services.udev;
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
19 # networkd link files are used early by udev to set up interfaces early.
20 # This must be done in stage 1 to avoid race conditions between udev and
22 # TODO move this into the initrd-network module when it exists
23 initrdLinkUnits = pkgs.runCommand "initrd-link-units" {} ''
25 ln -s ${udev}/lib/systemd/network/*.link $out/
26 ${lib.concatMapStringsSep "\n" (file: "ln -s ${file} $out/") (lib.mapAttrsToList (n: v: "${v.unit}/${n}") (lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units))}
29 extraUdevRules = pkgs.writeTextFile {
30 name = "extra-udev-rules";
31 text = cfg.extraRules;
32 destination = "/etc/udev/rules.d/99-local.rules";
35 extraHwdbFile = pkgs.writeTextFile {
36 name = "extra-hwdb-file";
38 destination = "/etc/udev/hwdb.d/99-local.hwdb";
42 # Miscellaneous devices.
43 KERNEL=="kvm", MODE="0666"
46 SUBSYSTEM=="input", KERNEL=="mice", TAG+="systemd"
49 # Perform substitutions in all udev rules files.
50 udevRulesFor = { name, udevPackages, udevPath, udev, systemd, binPackages, initrdBin ? null }: pkgs.runCommand name
51 { preferLocalBuild = true;
52 allowSubstitutes = false;
53 packages = unique (map toString udevPackages);
60 # Set a reasonable $PATH for programs called by udev rules.
61 echo 'ENV{PATH}="${udevPath}/bin:${udevPath}/sbin"' > $out/00-path.rules
63 # Add the udev rules from other packages.
64 for i in $packages; do
65 echo "Adding rules for package $i"
66 for j in $i/{etc,lib}/udev/rules.d/*; do
67 echo "Copying $j to $out/$(basename $j)"
68 cat $j > $out/$(basename $j)
72 # Fix some paths in the standard udev rules. Hacky.
73 for i in $out/*.rules; do
74 substituteInPlace $i \
75 --replace \"/sbin/modprobe \"${pkgs.kmod}/bin/modprobe \
76 --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
77 --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
78 --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
79 --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
80 --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
81 ${optionalString (initrdBin != null) ''
82 substituteInPlace $i --replace '/run/current-system/systemd' "${removeSuffix "/bin" initrdBin}"
86 echo -n "Checking that all programs called by relative paths in udev rules exist in ${udev}/lib/udev... "
87 import_progs=$(grep 'IMPORT{program}="[^/$]' $out/* |
88 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
89 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="[^/$]' |
90 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
91 for i in $import_progs $run_progs; do
92 if [[ ! -x ${udev}/lib/udev/$i && ! $i =~ socket:.* ]]; then
94 echo "$i is called in udev rules but not installed by udev"
100 echo -n "Checking that all programs called by absolute paths in udev rules exist... "
101 import_progs=$(grep 'IMPORT{program}="\/' $out/* |
102 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
103 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="/' |
104 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
105 for i in $import_progs $run_progs; do
106 # if the path refers to /run/current-system/systemd, replace with config.systemd.package
107 if [[ $i == /run/current-system/systemd* ]]; then
108 i="${systemd}/''${i#/run/current-system/systemd/}"
111 if [[ ! -x $i ]]; then
113 echo "$i is called in udev rules but is not executable or does not exist"
119 filesToFixup="$(for i in "$out"/*; do
120 grep -l '\B\(/usr\)\?/s\?bin' "$i" || :
123 if [ -n "$filesToFixup" ]; then
124 echo "Consider fixing the following udev rules:"
125 echo "$filesToFixup" | while read localFile; do
126 remoteFile="origin unknown"
127 for i in ${toString binPackages}; do
128 for j in "$i"/*/udev/rules.d/*; do
129 [ -e "$out/$(basename "$j")" ] || continue
130 [ "$(basename "$j")" = "$(basename "$localFile")" ] || continue
131 remoteFile="originally from $j"
136 grep -o '\B\(/usr\)\?/s\?bin/[^ "]\+' "$localFile" \
137 | sed -e ':r;N;''${s/\n/ and /;br};s/\n/, /g;br'
139 echo "$localFile ($remoteFile) contains references to $refs."
144 # If auto-configuration is disabled, then remove
145 # udev's 80-drivers.rules file, which contains rules for
146 # automatically calling modprobe.
147 ${optionalString (!config.boot.hardwareScan) ''
148 ln -s /dev/null $out/80-drivers.rules
152 hwdbBin = pkgs.runCommand "hwdb.bin"
153 { preferLocalBuild = true;
154 allowSubstitutes = false;
155 packages = unique (map toString ([udev] ++ cfg.packages));
158 mkdir -p etc/udev/hwdb.d
159 for i in $packages; do
160 echo "Adding hwdb files for package $i"
161 for j in $i/{etc,lib}/udev/hwdb.d/*; do
162 ln -s $j etc/udev/hwdb.d/$(basename $j)
166 echo "Generating hwdb database..."
167 # hwdb --update doesn't return error code even on errors!
168 res="$(${pkgs.buildPackages.udev}/bin/udevadm hwdb --update --root=$(pwd) 2>&1)"
170 [ -z "$(echo "$res" | egrep '^Error')" ]
171 mv etc/udev/hwdb.bin $out
174 compressFirmware = firmware: if (config.boot.kernelPackages.kernelAtLeast "5.3" && (firmware.compressFirmware or true)) then
175 pkgs.compressFirmwareXz firmware
179 # Udev has a 512-character limit for ENV{PATH}, so create a symlink
180 # tree to work around this.
181 udevPath = pkgs.buildEnv {
184 pathsToLink = [ "/bin" "/sbin" ];
185 ignoreCollisions = true;
195 boot.hardwareScan = mkOption {
198 description = lib.mdDoc ''
199 Whether to try to load kernel modules for all detected hardware.
200 Usually this does a good job of providing you with the modules
201 you need, but sometimes it can crash the system or cause other
207 enable = mkEnableOption (lib.mdDoc "udev") // {
211 packages = mkOption {
212 type = types.listOf types.path;
214 description = lib.mdDoc ''
215 List of packages containing {command}`udev` rules.
217 {file}`«pkg»/etc/udev/rules.d` and
218 {file}`«pkg»/lib/udev/rules.d`
225 type = types.listOf types.path;
227 description = lib.mdDoc ''
228 Packages added to the {env}`PATH` environment variable when
229 executing programs from Udev rules.
233 extraRules = mkOption {
236 ENV{ID_VENDOR_ID}=="046d", ENV{ID_MODEL_ID}=="0825", ENV{PULSE_IGNORE}="1"
239 description = lib.mdDoc ''
240 Additional {command}`udev` rules. They'll be written
241 into file {file}`99-local.rules`. Thus they are
242 read and applied after all other rules.
246 extraHwdb = mkOption {
249 evdev:input:b0003v05AFp8277*
250 KEYBOARD_KEY_70039=leftalt
251 KEYBOARD_KEY_700e2=leftctrl
254 description = lib.mdDoc ''
255 Additional {command}`hwdb` files. They'll be written
256 into file {file}`99-local.hwdb`. Thus they are
257 read after all other files.
263 hardware.firmware = mkOption {
264 type = types.listOf types.package;
266 description = lib.mdDoc ''
267 List of packages containing firmware files. Such files
268 will be loaded automatically if the kernel asks for them
269 (i.e., when it has detected specific hardware that requires
270 firmware to function). If multiple packages contain firmware
271 files with the same name, the first package in the list takes
272 precedence. Note that you must rebuild your system if you add
273 files to any of these directories.
275 apply = list: pkgs.buildEnv {
277 paths = map compressFirmware list;
278 pathsToLink = [ "/lib/firmware" ];
279 ignoreCollisions = true;
283 networking.usePredictableInterfaceNames = mkOption {
286 description = lib.mdDoc ''
287 Whether to assign [predictable names to network interfaces](http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames).
288 If enabled, interfaces
289 are assigned names that contain topology information
290 (e.g. `wlp3s0`) and thus should be stable
291 across reboots. If disabled, names depend on the order in
292 which interfaces are discovered by the kernel, which may
293 change randomly across reboots; for instance, you may find
294 `eth0` and `eth1` flipping
299 boot.initrd.services.udev = {
301 packages = mkOption {
302 type = types.listOf types.path;
305 description = lib.mdDoc ''
306 *This will only be used when systemd is used in stage 1.*
308 List of packages containing {command}`udev` rules that will be copied to stage 1.
310 {file}`«pkg»/etc/udev/rules.d` and
311 {file}`«pkg»/lib/udev/rules.d`
316 binPackages = mkOption {
317 type = types.listOf types.path;
320 description = lib.mdDoc ''
321 *This will only be used when systemd is used in stage 1.*
323 Packages to search for binaries that are referenced by the udev rules in stage 1.
324 This list always contains /bin of the initrd.
332 SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1D:60:B9:6D:4F", KERNEL=="eth*", NAME="my_fast_network_card"
335 description = lib.mdDoc ''
336 {command}`udev` rules to include in the initrd
337 *only*. They'll be written into file
338 {file}`99-local.rules`. Thus they are read and applied
339 after the essential initrd rules.
348 ###### implementation
350 config = mkIf cfg.enable {
352 services.udev.extraRules = nixosRules;
354 services.udev.packages = [ extraUdevRules extraHwdbFile ];
356 services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.util-linux udev ];
358 boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
360 boot.initrd.extraUdevRulesCommands = optionalString (!config.boot.initrd.systemd.enable && config.boot.initrd.services.udev.rules != "")
362 cat <<'EOF' > $out/99-local.rules
363 ${config.boot.initrd.services.udev.rules}
367 boot.initrd.systemd.additionalUpstreamUnits = [
368 # TODO: "initrd-udevadm-cleanup-db.service" is commented out because of https://github.com/systemd/systemd/issues/12953
369 "systemd-udevd-control.socket"
370 "systemd-udevd-kernel.socket"
371 "systemd-udevd.service"
372 "systemd-udev-settle.service"
373 "systemd-udev-trigger.service"
375 boot.initrd.systemd.storePaths = [
376 "${config.boot.initrd.systemd.package}/lib/systemd/systemd-udevd"
377 "${config.boot.initrd.systemd.package}/lib/udev/ata_id"
378 "${config.boot.initrd.systemd.package}/lib/udev/cdrom_id"
379 "${config.boot.initrd.systemd.package}/lib/udev/scsi_id"
380 "${config.boot.initrd.systemd.package}/lib/udev/rules.d"
381 ] ++ map (x: "${x}/bin") config.boot.initrd.services.udev.binPackages;
383 # Generate the udev rules for the initrd
384 boot.initrd.systemd.contents = {
385 "/etc/udev/rules.d".source = udevRulesFor {
386 name = "initrd-udev-rules";
387 initrdBin = config.boot.initrd.systemd.contents."/bin".source;
388 udevPackages = config.boot.initrd.services.udev.packages;
389 udevPath = config.boot.initrd.systemd.contents."/bin".source;
390 udev = config.boot.initrd.systemd.package;
391 systemd = config.boot.initrd.systemd.package;
392 binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ];
394 "/etc/systemd/network".source = initrdLinkUnits;
396 # Insert initrd rules
397 boot.initrd.services.udev.packages = [
399 (mkIf (config.boot.initrd.services.udev.rules != "") (pkgs.writeTextFile {
400 name = "initrd-udev-rules";
401 destination = "/etc/udev/rules.d/99-local.rules";
402 text = config.boot.initrd.services.udev.rules;
408 "udev/rules.d".source = udevRulesFor {
410 udevPackages = cfg.packages;
411 systemd = config.systemd.package;
412 binPackages = cfg.packages;
413 inherit udevPath udev;
415 "udev/hwdb.bin".source = hwdbBin;
418 system.requiredKernelConfig = with config.lib.kernelConfig; [
420 (isYes "INOTIFY_USER")
424 # We don't place this into `extraModprobeConfig` so that stage-1 ramdisk doesn't bloat.
425 environment.etc."modprobe.d/firmware.conf".text = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
427 system.activationScripts.udevd =
429 # The deprecated hotplug uevent helper is not used anymore
430 if [ -e /proc/sys/kernel/hotplug ]; then
431 echo "" > /proc/sys/kernel/hotplug
434 # Allow the kernel to find our firmware.
435 if [ -e /sys/module/firmware_class/parameters/path ]; then
436 echo -n "${config.hardware.firmware}/lib/firmware" > /sys/module/firmware_class/parameters/path
440 systemd.services.systemd-udevd =
441 { restartTriggers = cfg.packages;
447 (mkRenamedOptionModule [ "services" "udev" "initrdRules" ] [ "boot" "initrd" "services" "udev" "rules" ])