aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xlib/python/qmk/cli/doctor.py288
-rw-r--r--lib/python/qmk/os_helpers/__init__.py165
-rw-r--r--lib/python/qmk/os_helpers/linux/__init__.py140
3 files changed, 308 insertions, 285 deletions
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor.py
index ea1113c64..70f32911a 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor.py
@@ -3,293 +3,13 @@
3Check out the user's QMK environment and make sure it's ready to compile. 3Check out the user's QMK environment and make sure it's ready to compile.
4""" 4"""
5import platform 5import platform
6import re
7import shutil
8import subprocess
9from pathlib import Path
10from enum import Enum
11 6
12from milc import cli 7from milc import cli
13from milc.questions import yesno 8from milc.questions import yesno
14from qmk import submodules 9from qmk import submodules
15from qmk.constants import QMK_FIRMWARE 10from qmk.constants import QMK_FIRMWARE
16from qmk.commands import run 11from qmk.commands import run
17 12from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo
18
19class CheckStatus(Enum):
20 OK = 1
21 WARNING = 2
22 ERROR = 3
23
24
25ESSENTIAL_BINARIES = {
26 'dfu-programmer': {},
27 'avrdude': {},
28 'dfu-util': {},
29 'avr-gcc': {
30 'version_arg': '-dumpversion'
31 },
32 'arm-none-eabi-gcc': {
33 'version_arg': '-dumpversion'
34 },
35 'bin/qmk': {},
36}
37
38
39def _udev_rule(vid, pid=None, *args):
40 """ Helper function that return udev rules
41 """
42 rule = ""
43 if pid:
44 rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", TAG+="uaccess"' % (
45 vid,
46 pid,
47 )
48 else:
49 rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", TAG+="uaccess"' % vid
50 if args:
51 rule = ', '.join([rule, *args])
52 return rule
53
54
55def _deprecated_udev_rule(vid, pid=None):
56 """ Helper function that return udev rules
57
58 Note: these are no longer the recommended rules, this is just used to check for them
59 """
60 if pid:
61 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", MODE:="0666"' % (vid, pid)
62 else:
63 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", MODE:="0666"' % vid
64
65
66def parse_gcc_version(version):
67 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
68
69 return {
70 'major': int(m.group(1)),
71 'minor': int(m.group(2)) if m.group(2) else 0,
72 'patch': int(m.group(3)) if m.group(3) else 0,
73 }
74
75
76def check_arm_gcc_version():
77 """Returns True if the arm-none-eabi-gcc version is not known to cause problems.
78 """
79 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']:
80 version_number = ESSENTIAL_BINARIES['arm-none-eabi-gcc']['output'].strip()
81 cli.log.info('Found arm-none-eabi-gcc version %s', version_number)
82
83 return CheckStatus.OK # Right now all known arm versions are ok
84
85
86def check_avr_gcc_version():
87 """Returns True if the avr-gcc version is not known to cause problems.
88 """
89 rc = CheckStatus.ERROR
90 if 'output' in ESSENTIAL_BINARIES['avr-gcc']:
91 version_number = ESSENTIAL_BINARIES['avr-gcc']['output'].strip()
92
93 cli.log.info('Found avr-gcc version %s', version_number)
94 rc = CheckStatus.OK
95
96 parsed_version = parse_gcc_version(version_number)
97 if parsed_version['major'] > 8:
98 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')
99 rc = CheckStatus.WARNING
100
101 return rc
102
103
104def check_avrdude_version():
105 if 'output' in ESSENTIAL_BINARIES['avrdude']:
106 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]
107 version_number = last_line.split()[2][:-1]
108 cli.log.info('Found avrdude version %s', version_number)
109
110 return CheckStatus.OK
111
112
113def check_dfu_util_version():
114 if 'output' in ESSENTIAL_BINARIES['dfu-util']:
115 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
116 version_number = first_line.split()[1]
117 cli.log.info('Found dfu-util version %s', version_number)
118
119 return CheckStatus.OK
120
121
122def check_dfu_programmer_version():
123 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:
124 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
125 version_number = first_line.split()[1]
126 cli.log.info('Found dfu-programmer version %s', version_number)
127
128 return CheckStatus.OK
129
130
131def check_binaries():
132 """Iterates through ESSENTIAL_BINARIES and tests them.
133 """
134 ok = True
135
136 for binary in sorted(ESSENTIAL_BINARIES):
137 if not is_executable(binary):
138 ok = False
139
140 return ok
141
142
143def check_submodules():
144 """Iterates through all submodules to make sure they're cloned and up to date.
145 """
146 for submodule in submodules.status().values():
147 if submodule['status'] is None:
148 cli.log.error('Submodule %s has not yet been cloned!', submodule['name'])
149 return CheckStatus.ERROR
150 elif not submodule['status']:
151 cli.log.warning('Submodule %s is not up to date!', submodule['name'])
152 return CheckStatus.WARNING
153
154 return CheckStatus.OK
155
156
157def check_git_repo():
158 """Checks that the .git directory exists inside QMK_HOME.
159
160 This is a decent enough indicator that the qmk_firmware directory is a
161 proper Git repository, rather than a .zip download from GitHub.
162 """
163 dot_git_dir = QMK_FIRMWARE / '.git'
164
165 return CheckStatus.OK if dot_git_dir.is_dir() else CheckStatus.WARNING
166
167
168def check_udev_rules():
169 """Make sure the udev rules look good.
170 """
171 rc = CheckStatus.OK
172 udev_dir = Path("/etc/udev/rules.d/")
173 desired_rules = {
174 'atmel-dfu': {
175 _udev_rule("03eb", "2fef"), # ATmega16U2
176 _udev_rule("03eb", "2ff0"), # ATmega32U2
177 _udev_rule("03eb", "2ff3"), # ATmega16U4
178 _udev_rule("03eb", "2ff4"), # ATmega32U4
179 _udev_rule("03eb", "2ff9"), # AT90USB64
180 _udev_rule("03eb", "2ffb") # AT90USB128
181 },
182 'kiibohd': {_udev_rule("1c11", "b007")},
183 'stm32': {
184 _udev_rule("1eaf", "0003"), # STM32duino
185 _udev_rule("0483", "df11") # STM32 DFU
186 },
187 'bootloadhid': {_udev_rule("16c0", "05df")},
188 'usbasploader': {_udev_rule("16c0", "05dc")},
189 'massdrop': {_udev_rule("03eb", "6124", 'ENV{ID_MM_DEVICE_IGNORE}="1"')},
190 'caterina': {
191 # Spark Fun Electronics
192 _udev_rule("1b4f", "9203", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 3V3/8MHz
193 _udev_rule("1b4f", "9205", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 5V/16MHz
194 _udev_rule("1b4f", "9207", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # LilyPad 3V3/8MHz (and some Pro Micro clones)
195 # Pololu EleCTRONICS
196 _udev_rule("1ffb", "0101", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # A-Star 32U4
197 # Arduino SA
198 _udev_rule("2341", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
199 _udev_rule("2341", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Micro
200 # Adafruit INDUSTRIES llC
201 _udev_rule("239a", "000c", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Feather 32U4
202 _udev_rule("239a", "000d", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 3V3/8MHz
203 _udev_rule("239a", "000e", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 5V/16MHz
204 # dog hunter ag
205 _udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
206 _udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro
207 }
208 }
209
210 # These rules are no longer recommended, only use them to check for their presence.
211 deprecated_rules = {
212 'atmel-dfu': {_deprecated_udev_rule("03eb", "2ff4"), _deprecated_udev_rule("03eb", "2ffb"), _deprecated_udev_rule("03eb", "2ff0")},
213 'kiibohd': {_deprecated_udev_rule("1c11")},
214 'stm32': {_deprecated_udev_rule("1eaf", "0003"), _deprecated_udev_rule("0483", "df11")},
215 'bootloadhid': {_deprecated_udev_rule("16c0", "05df")},
216 'caterina': {'ATTRS{idVendor}=="2a03", ENV{ID_MM_DEVICE_IGNORE}="1"', 'ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"'},
217 'tmk': {_deprecated_udev_rule("feed")}
218 }
219
220 if udev_dir.exists():
221 udev_rules = [rule_file for rule_file in udev_dir.glob('*.rules')]
222 current_rules = set()
223
224 # Collect all rules from the config files
225 for rule_file in udev_rules:
226 for line in rule_file.read_text().split('\n'):
227 line = line.strip()
228 if not line.startswith("#") and len(line):
229 current_rules.add(line)
230
231 # Check if the desired rules are among the currently present rules
232 for bootloader, rules in desired_rules.items():
233 if not rules.issubset(current_rules):
234 deprecated_rule = deprecated_rules.get(bootloader)
235 if deprecated_rule and deprecated_rule.issubset(current_rules):
236 cli.log.warning("{fg_yellow}Found old, deprecated udev rules for '%s' boards. The new rules on https://docs.qmk.fm/#/faq_build?id=linux-udev-rules offer better security with the same functionality.", bootloader)
237 else:
238 # For caterina, check if ModemManager is running
239 if bootloader == "caterina":
240 if check_modem_manager():
241 rc = CheckStatus.WARNING
242 cli.log.warning("{fg_yellow}Detected ModemManager without the necessary udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro.")
243 rc = CheckStatus.WARNING
244 cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE)
245
246 else:
247 cli.log.warning("{fg_yellow}'%s' does not exist. Skipping udev rule checking...", udev_dir)
248
249 return rc
250
251
252def check_systemd():
253 """Check if it's a systemd system
254 """
255 return bool(shutil.which("systemctl"))
256
257
258def check_modem_manager():
259 """Returns True if ModemManager is running.
260
261 """
262 if check_systemd():
263 mm_check = run(["systemctl", "--quiet", "is-active", "ModemManager.service"], timeout=10)
264 if mm_check.returncode == 0:
265 return True
266 else:
267 """(TODO): Add check for non-systemd systems
268 """
269 return False
270
271
272def is_executable(command):
273 """Returns True if command exists and can be executed.
274 """
275 # Make sure the command is in the path.
276 res = shutil.which(command)
277 if res is None:
278 cli.log.error("{fg_red}Can't find %s in your path.", command)
279 return False
280
281 # Make sure the command can be executed
282 version_arg = ESSENTIAL_BINARIES[command].get('version_arg', '--version')
283 check = run([command, version_arg], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5, universal_newlines=True)
284
285 ESSENTIAL_BINARIES[command]['output'] = check.stdout
286
287 if check.returncode in [0, 1]: # Older versions of dfu-programmer exit 1
288 cli.log.debug('Found {fg_cyan}%s', command)
289 return True
290
291 cli.log.error("{fg_red}Can't run `%s %s`", command, version_arg)
292 return False
293 13
294 14
295def os_tests(): 15def os_tests():
@@ -312,6 +32,7 @@ def os_test_linux():
312 """Run the Linux specific tests. 32 """Run the Linux specific tests.
313 """ 33 """
314 cli.log.info("Detected {fg_cyan}Linux.") 34 cli.log.info("Detected {fg_cyan}Linux.")
35 from qmk.os_helpers.linux import check_udev_rules
315 36
316 return check_udev_rules() 37 return check_udev_rules()
317 38
@@ -370,10 +91,7 @@ def doctor(cli):
370 status = CheckStatus.ERROR 91 status = CheckStatus.ERROR
371 92
372 # Make sure the tools are at the correct version 93 # Make sure the tools are at the correct version
373 ver_ok = [] 94 ver_ok = check_binary_versions()
374 for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version):
375 ver_ok.append(check())
376
377 if CheckStatus.ERROR in ver_ok: 95 if CheckStatus.ERROR in ver_ok:
378 status = CheckStatus.ERROR 96 status = CheckStatus.ERROR
379 elif CheckStatus.WARNING in ver_ok and status == CheckStatus.OK: 97 elif CheckStatus.WARNING in ver_ok and status == CheckStatus.OK:
diff --git a/lib/python/qmk/os_helpers/__init__.py b/lib/python/qmk/os_helpers/__init__.py
new file mode 100644
index 000000000..3f64a63a3
--- /dev/null
+++ b/lib/python/qmk/os_helpers/__init__.py
@@ -0,0 +1,165 @@
1"""OS-agnostic helper functions
2"""
3from enum import Enum
4import re
5import shutil
6import subprocess
7
8from milc import cli
9from qmk.commands import run
10from qmk import submodules
11from qmk.constants import QMK_FIRMWARE
12
13
14class CheckStatus(Enum):
15 OK = 1
16 WARNING = 2
17 ERROR = 3
18
19
20ESSENTIAL_BINARIES = {
21 'dfu-programmer': {},
22 'avrdude': {},
23 'dfu-util': {},
24 'avr-gcc': {
25 'version_arg': '-dumpversion'
26 },
27 'arm-none-eabi-gcc': {
28 'version_arg': '-dumpversion'
29 },
30 'bin/qmk': {},
31}
32
33
34def parse_gcc_version(version):
35 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
36
37 return {
38 'major': int(m.group(1)),
39 'minor': int(m.group(2)) if m.group(2) else 0,
40 'patch': int(m.group(3)) if m.group(3) else 0,
41 }
42
43
44def check_arm_gcc_version():
45 """Returns True if the arm-none-eabi-gcc version is not known to cause problems.
46 """
47 if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']:
48 version_number = ESSENTIAL_BINARIES['arm-none-eabi-gcc']['output'].strip()
49 cli.log.info('Found arm-none-eabi-gcc version %s', version_number)
50
51 return CheckStatus.OK # Right now all known arm versions are ok
52
53
54def check_avr_gcc_version():
55 """Returns True if the avr-gcc version is not known to cause problems.
56 """
57 rc = CheckStatus.ERROR
58 if 'output' in ESSENTIAL_BINARIES['avr-gcc']:
59 version_number = ESSENTIAL_BINARIES['avr-gcc']['output'].strip()
60
61 cli.log.info('Found avr-gcc version %s', version_number)
62 rc = CheckStatus.OK
63
64 parsed_version = parse_gcc_version(version_number)
65 if parsed_version['major'] > 8:
66 cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')
67 rc = CheckStatus.WARNING
68
69 return rc
70
71
72def check_avrdude_version():
73 if 'output' in ESSENTIAL_BINARIES['avrdude']:
74 last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]
75 version_number = last_line.split()[2][:-1]
76 cli.log.info('Found avrdude version %s', version_number)
77
78 return CheckStatus.OK
79
80
81def check_dfu_util_version():
82 if 'output' in ESSENTIAL_BINARIES['dfu-util']:
83 first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]
84 version_number = first_line.split()[1]
85 cli.log.info('Found dfu-util version %s', version_number)
86
87 return CheckStatus.OK
88
89
90def check_dfu_programmer_version():
91 if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:
92 first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]
93 version_number = first_line.split()[1]
94 cli.log.info('Found dfu-programmer version %s', version_number)
95
96 return CheckStatus.OK
97
98
99def check_binaries():
100 """Iterates through ESSENTIAL_BINARIES and tests them.
101 """
102 ok = True
103
104 for binary in sorted(ESSENTIAL_BINARIES):
105 if not is_executable(binary):
106 ok = False
107
108 return ok
109
110
111def check_binary_versions():
112 """Check the versions of ESSENTIAL_BINARIES
113 """
114 versions = []
115 for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version):
116 versions.append(check())
117 return versions
118
119
120def check_submodules():
121 """Iterates through all submodules to make sure they're cloned and up to date.
122 """
123 for submodule in submodules.status().values():
124 if submodule['status'] is None:
125 cli.log.error('Submodule %s has not yet been cloned!', submodule['name'])
126 return CheckStatus.ERROR
127 elif not submodule['status']:
128 cli.log.warning('Submodule %s is not up to date!', submodule['name'])
129 return CheckStatus.WARNING
130
131 return CheckStatus.OK
132
133
134def is_executable(command):
135 """Returns True if command exists and can be executed.
136 """
137 # Make sure the command is in the path.
138 res = shutil.which(command)
139 if res is None:
140 cli.log.error("{fg_red}Can't find %s in your path.", command)
141 return False
142
143 # Make sure the command can be executed
144 version_arg = ESSENTIAL_BINARIES[command].get('version_arg', '--version')
145 check = run([command, version_arg], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5, universal_newlines=True)
146
147 ESSENTIAL_BINARIES[command]['output'] = check.stdout
148
149 if check.returncode in [0, 1]: # Older versions of dfu-programmer exit 1
150 cli.log.debug('Found {fg_cyan}%s', command)
151 return True
152
153 cli.log.error("{fg_red}Can't run `%s %s`", command, version_arg)
154 return False
155
156
157def check_git_repo():
158 """Checks that the .git directory exists inside QMK_HOME.
159
160 This is a decent enough indicator that the qmk_firmware directory is a
161 proper Git repository, rather than a .zip download from GitHub.
162 """
163 dot_git_dir = QMK_FIRMWARE / '.git'
164
165 return CheckStatus.OK if dot_git_dir.is_dir() else CheckStatus.WARNING
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/os_helpers/linux/__init__.py
new file mode 100644
index 000000000..86850bf28
--- /dev/null
+++ b/lib/python/qmk/os_helpers/linux/__init__.py
@@ -0,0 +1,140 @@
1"""OS-specific functions for: Linux
2"""
3from pathlib import Path
4import shutil
5
6from milc import cli
7from qmk.constants import QMK_FIRMWARE
8from qmk.commands import run
9from qmk.os_helpers import CheckStatus
10
11
12def _udev_rule(vid, pid=None, *args):
13 """ Helper function that return udev rules
14 """
15 rule = ""
16 if pid:
17 rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", TAG+="uaccess"' % (
18 vid,
19 pid,
20 )
21 else:
22 rule = 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", TAG+="uaccess"' % vid
23 if args:
24 rule = ', '.join([rule, *args])
25 return rule
26
27
28def _deprecated_udev_rule(vid, pid=None):
29 """ Helper function that return udev rules
30
31 Note: these are no longer the recommended rules, this is just used to check for them
32 """
33 if pid:
34 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", ATTRS{idProduct}=="%s", MODE:="0666"' % (vid, pid)
35 else:
36 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", MODE:="0666"' % vid
37
38
39def check_udev_rules():
40 """Make sure the udev rules look good.
41 """
42 rc = CheckStatus.OK
43 udev_dir = Path("/etc/udev/rules.d/")
44 desired_rules = {
45 'atmel-dfu': {
46 _udev_rule("03eb", "2fef"), # ATmega16U2
47 _udev_rule("03eb", "2ff0"), # ATmega32U2
48 _udev_rule("03eb", "2ff3"), # ATmega16U4
49 _udev_rule("03eb", "2ff4"), # ATmega32U4
50 _udev_rule("03eb", "2ff9"), # AT90USB64
51 _udev_rule("03eb", "2ffb") # AT90USB128
52 },
53 'kiibohd': {_udev_rule("1c11", "b007")},
54 'stm32': {
55 _udev_rule("1eaf", "0003"), # STM32duino
56 _udev_rule("0483", "df11") # STM32 DFU
57 },
58 'bootloadhid': {_udev_rule("16c0", "05df")},
59 'usbasploader': {_udev_rule("16c0", "05dc")},
60 'massdrop': {_udev_rule("03eb", "6124", 'ENV{ID_MM_DEVICE_IGNORE}="1"')},
61 'caterina': {
62 # Spark Fun Electronics
63 _udev_rule("1b4f", "9203", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 3V3/8MHz
64 _udev_rule("1b4f", "9205", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 5V/16MHz
65 _udev_rule("1b4f", "9207", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # LilyPad 3V3/8MHz (and some Pro Micro clones)
66 # Pololu Electronics
67 _udev_rule("1ffb", "0101", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # A-Star 32U4
68 # Arduino SA
69 _udev_rule("2341", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
70 _udev_rule("2341", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Micro
71 # Adafruit Industries LLC
72 _udev_rule("239a", "000c", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Feather 32U4
73 _udev_rule("239a", "000d", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 3V3/8MHz
74 _udev_rule("239a", "000e", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 5V/16MHz
75 # dog hunter AG
76 _udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
77 _udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro
78 }
79 }
80
81 # These rules are no longer recommended, only use them to check for their presence.
82 deprecated_rules = {
83 'atmel-dfu': {_deprecated_udev_rule("03eb", "2ff4"), _deprecated_udev_rule("03eb", "2ffb"), _deprecated_udev_rule("03eb", "2ff0")},
84 'kiibohd': {_deprecated_udev_rule("1c11")},
85 'stm32': {_deprecated_udev_rule("1eaf", "0003"), _deprecated_udev_rule("0483", "df11")},
86 'bootloadhid': {_deprecated_udev_rule("16c0", "05df")},
87 'caterina': {'ATTRS{idVendor}=="2a03", ENV{ID_MM_DEVICE_IGNORE}="1"', 'ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"'},
88 'tmk': {_deprecated_udev_rule("feed")}
89 }
90
91 if udev_dir.exists():
92 udev_rules = [rule_file for rule_file in udev_dir.glob('*.rules')]
93 current_rules = set()
94
95 # Collect all rules from the config files
96 for rule_file in udev_rules:
97 for line in rule_file.read_text().split('\n'):
98 line = line.strip()
99 if not line.startswith("#") and len(line):
100 current_rules.add(line)
101
102 # Check if the desired rules are among the currently present rules
103 for bootloader, rules in desired_rules.items():
104 if not rules.issubset(current_rules):
105 deprecated_rule = deprecated_rules.get(bootloader)
106 if deprecated_rule and deprecated_rule.issubset(current_rules):
107 cli.log.warning("{fg_yellow}Found old, deprecated udev rules for '%s' boards. The new rules on https://docs.qmk.fm/#/faq_build?id=linux-udev-rules offer better security with the same functionality.", bootloader)
108 else:
109 # For caterina, check if ModemManager is running
110 if bootloader == "caterina":
111 if check_modem_manager():
112 rc = CheckStatus.WARNING
113 cli.log.warning("{fg_yellow}Detected ModemManager without the necessary udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro.")
114 rc = CheckStatus.WARNING
115 cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE)
116
117 else:
118 cli.log.warning("{fg_yellow}'%s' does not exist. Skipping udev rule checking...", udev_dir)
119
120 return rc
121
122
123def check_systemd():
124 """Check if it's a systemd system
125 """
126 return bool(shutil.which("systemctl"))
127
128
129def check_modem_manager():
130 """Returns True if ModemManager is running.
131
132 """
133 if check_systemd():
134 mm_check = run(["systemctl", "--quiet", "is-active", "ModemManager.service"], timeout=10)
135 if mm_check.returncode == 0:
136 return True
137 else:
138 """(TODO): Add check for non-systemd systems
139 """
140 return False