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