1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 inherit (pkgs) stdenv writeText procps;
8
9 udev = config.systemd.package;
10
11 cfg = config.services.udev;
12
13 extraUdevRules = pkgs.writeTextFile {
14 name = "extra-udev-rules";
15 text = cfg.extraRules;
16 destination = "/etc/udev/rules.d/10-local.rules";
17 };
18
19 nixosRules = ''
20 # Miscellaneous devices.
21 KERNEL=="kvm", MODE="0666"
22 KERNEL=="kqemu", MODE="0666"
23
24 # Needed for gpm.
25 SUBSYSTEM=="input", KERNEL=="mice", TAG+="systemd"
26 '';
27
28 # Perform substitutions in all udev rules files.
29 udevRules = stdenv.mkDerivation {
30 name = "udev-rules";
31
32 preferLocalBuild = true;
33 allowSubstitutes = false;
34
35 buildCommand = ''
36 mkdir -p $out
37 shopt -s nullglob
38 set +o pipefail
39
40 # Set a reasonable $PATH for programs called by udev rules.
41 echo 'ENV{PATH}="${udevPath}/bin:${udevPath}/sbin"' > $out/00-path.rules
42
43 # Add the udev rules from other packages.
44 for i in ${toString cfg.packages}; do
45 echo "Adding rules for package $i"
46 for j in $i/{etc,lib}/udev/rules.d/*; do
47 echo "Copying $j to $out/$(basename $j)"
48 cat $j > $out/$(basename $j)
49 done
50 done
51
52 # Fix some paths in the standard udev rules. Hacky.
53 for i in $out/*.rules; do
54 substituteInPlace $i \
55 --replace \"/sbin/modprobe \"${config.system.sbin.modprobe}/sbin/modprobe \
56 --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
57 --replace \"/sbin/blkid \"${pkgs.utillinux}/sbin/blkid \
58 --replace \"/bin/mount \"${pkgs.utillinux}/bin/mount
59 done
60
61 echo -n "Checking that all programs called by relative paths in udev rules exist in ${udev}/lib/udev... "
62 import_progs=$(grep 'IMPORT{program}="[^/$]' $out/* |
63 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
64 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="[^/$]' |
65 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
66 for i in $import_progs $run_progs; do
67 if [[ ! -x ${pkgs.udev}/lib/udev/$i && ! $i =~ socket:.* ]]; then
68 echo "FAIL"
69 echo "$i is called in udev rules but not installed by udev"
70 exit 1
71 fi
72 done
73 echo "OK"
74
75 echo -n "Checking that all programs called by absolute paths in udev rules exist... "
76 import_progs=$(grep 'IMPORT{program}="\/' $out/* |
77 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
78 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="/' |
79 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
80 for i in $import_progs $run_progs; do
81 if [[ ! -x $i ]]; then
82 echo "FAIL"
83 echo "$i is called in udev rules but not installed by udev"
84 exit 1
85 fi
86 done
87 echo "OK"
88
89 echo "Consider fixing the following udev rules:"
90 for i in ${toString cfg.packages}; do
91 grep -l '\(RUN+\|IMPORT{program}\)="\(/usr\)\?/s\?bin' $i/*/udev/rules.d/* || true
92 done
93
94 ${optionalString config.networking.usePredictableInterfaceNames ''
95 cp ${./80-net-setup-link.rules} $out/80-net-setup-link.rules
96 ''}
97
98 # If auto-configuration is disabled, then remove
99 # udev's 80-drivers.rules file, which contains rules for
100 # automatically calling modprobe.
101 ${optionalString (!config.boot.hardwareScan) ''
102 ln -s /dev/null $out/80-drivers.rules
103 ''}
104 ''; # */
105 };
106
107 # Udev has a 512-character limit for ENV{PATH}, so create a symlink
108 # tree to work around this.
109 udevPath = pkgs.buildEnv {
110 name = "udev-path";
111 paths = cfg.path;
112 pathsToLink = [ "/bin" "/sbin" ];
113 ignoreCollisions = true;
114 };
115
116in
117
118{
119
120 ###### interface
121
122 options = {
123
124 boot.hardwareScan = mkOption {
125 type = types.bool;
126 default = true;
127 description = ''
128 Whether to try to load kernel modules for all detected hardware.
129 Usually this does a good job of providing you with the modules
130 you need, but sometimes it can crash the system or cause other
131 nasty effects.
132 '';
133 };
134
135 services.udev = {
136
137 packages = mkOption {
138 type = types.listOf types.path;
139 default = [];
140 description = ''
141 List of packages containing <command>udev</command> rules.
142 All files found in
143 <filename><replaceable>pkg</replaceable>/etc/udev/rules.d</filename> and
144 <filename><replaceable>pkg</replaceable>/lib/udev/rules.d</filename>
145 will be included.
146 '';
147 };
148
149 path = mkOption {
150 type = types.listOf types.path;
151 default = [];
152 description = ''
153 Packages added to the <envar>PATH</envar> environment variable when
154 executing programs from Udev rules.
155 '';
156 };
157
158 extraRules = mkOption {
159 default = "";
160 example = ''
161 KERNEL=="eth*", ATTR{address}=="00:1D:60:B9:6D:4F", NAME="my_fast_network_card"
162 '';
163 type = types.lines;
164 description = ''
165 Additional <command>udev</command> rules. They'll be written
166 into file <filename>10-local.rules</filename>. Thus they are
167 read before all other rules.
168 '';
169 };
170
171 };
172
173 hardware.firmware = mkOption {
174 type = types.listOf types.package;
175 default = [];
176 description = ''
177 List of packages containing firmware files. Such files
178 will be loaded automatically if the kernel asks for them
179 (i.e., when it has detected specific hardware that requires
180 firmware to function). If multiple packages contain firmware
181 files with the same name, the first package in the list takes
182 precedence. Note that you must rebuild your system if you add
183 files to any of these directories. For quick testing,
184 put firmware files in <filename>/root/test-firmware</filename>
185 and add that directory to the list.
186 '';
187 apply = list: pkgs.buildEnv {
188 name = "firmware";
189 paths = list;
190 pathsToLink = [ "/lib/firmware" ];
191 ignoreCollisions = true;
192 };
193 };
194
195 networking.usePredictableInterfaceNames = mkOption {
196 default = true;
197 type = types.bool;
198 description = ''
199 Whether to assign <link
200 xlink:href='http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames'>predictable
201 names to network interfaces</link>. If enabled, interfaces
202 are assigned names that contain topology information
203 (e.g. <literal>wlp3s0</literal>) and thus should be stable
204 across reboots. If disabled, names depend on the order in
205 which interfaces are discovered by the kernel, which may
206 change randomly across reboots; for instance, you may find
207 <literal>eth0</literal> and <literal>eth1</literal> flipping
208 unpredictably.
209 '';
210 };
211
212 };
213
214
215 ###### implementation
216
217 config = mkIf (!config.boot.isContainer) {
218
219 services.udev.extraRules = nixosRules;
220
221 services.udev.packages = [ extraUdevRules ];
222
223 services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.utillinux udev ];
224
225 environment.etc =
226 [ { source = udevRules;
227 target = "udev/rules.d";
228 }
229 ];
230
231 system.requiredKernelConfig = with config.lib.kernelConfig; [
232 (isEnabled "UNIX")
233 (isYes "INOTIFY_USER")
234 (isYes "NET")
235 ];
236
237 boot.extraModprobeConfig = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
238
239 system.activationScripts.udevd =
240 ''
241 # The deprecated hotplug uevent helper is not used anymore
242 if [ -e /proc/sys/kernel/hotplug ]; then
243 echo "" > /proc/sys/kernel/hotplug
244 fi
245
246 # Regenerate the hardware database /var/lib/udev/hwdb.bin
247 # whenever systemd changes.
248 if [ ! -e /var/lib/udev/prev-systemd -o "$(readlink /var/lib/udev/prev-systemd)" != ${config.systemd.package} ]; then
249 echo "regenerating udev hardware database..."
250 ${config.systemd.package}/bin/udevadm hwdb --update && ln -sfn ${config.systemd.package} /var/lib/udev/prev-systemd
251 fi
252
253 # Allow the kernel to find our firmware.
254 if [ -e /sys/module/firmware_class/parameters/path ]; then
255 echo -n "${config.hardware.firmware}/lib/firmware" > /sys/module/firmware_class/parameters/path
256 fi
257 '';
258
259 systemd.services.systemd-udevd =
260 { environment.MODULE_DIR = "/run/booted-system/kernel-modules/lib/modules";
261 };
262
263 };
264}