aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/cli.md18
-rwxr-xr-xlib/python/qmk/cli/doctor.py239
-rw-r--r--lib/python/qmk/questions.py97
-rw-r--r--lib/python/qmk/submodules.py71
4 files changed, 360 insertions, 65 deletions
diff --git a/docs/cli.md b/docs/cli.md
index 1c0952722..4f328a75a 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -141,14 +141,28 @@ qmk docs [-p PORT]
141 141
142## `qmk doctor` 142## `qmk doctor`
143 143
144This command examines your environment and alerts you to potential build or flash problems. 144This command examines your environment and alerts you to potential build or flash problems. It can fix many of them if you want it to.
145 145
146**Usage**: 146**Usage**:
147 147
148``` 148```
149qmk doctor 149qmk doctor [-y] [-n]
150``` 150```
151 151
152**Examples**:
153
154Check your environment for problems and prompt to fix them:
155
156 qmk doctor
157
158Check your environment and automatically fix any problems found:
159
160 qmk doctor -y
161
162Check your environment and report problems only:
163
164 qmk doctor -n
165
152## `qmk json-keymap` 166## `qmk json-keymap`
153 167
154Creates a keymap.c from a QMK Configurator export. 168Creates a keymap.c from a QMK Configurator export.
diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor.py
index 6ddc5571b..013bf7943 100755
--- a/lib/python/qmk/cli/doctor.py
+++ b/lib/python/qmk/cli/doctor.py
@@ -1,14 +1,18 @@
1"""QMK Python Doctor 1"""QMK Doctor
2 2
3Check up for QMK environment. 3Check out the user's QMK environment and make sure it's ready to compile.
4""" 4"""
5import os
6import platform 5import platform
7import shutil 6import shutil
8import subprocess 7import subprocess
9import glob 8from pathlib import Path
10 9
11from milc import cli 10from milc import cli
11from qmk import submodules
12from qmk.questions import yesno
13
14ESSENTIAL_BINARIES = ['dfu-programmer', 'avrdude', 'dfu-util', 'avr-gcc', 'arm-none-eabi-gcc', 'bin/qmk']
15ESSENTIAL_SUBMODULES = ['lib/chibios', 'lib/lufa']
12 16
13 17
14def _udev_rule(vid, pid=None): 18def _udev_rule(vid, pid=None):
@@ -20,6 +24,137 @@ def _udev_rule(vid, pid=None):
20 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", MODE:="0666"' % vid 24 return 'SUBSYSTEMS=="usb", ATTRS{idVendor}=="%s", MODE:="0666"' % vid
21 25
22 26
27def check_binaries():
28 """Iterates through ESSENTIAL_BINARIES and tests them.
29 """
30 ok = True
31
32 for binary in ESSENTIAL_BINARIES:
33 if not is_executable(binary):
34 ok = False
35
36 return ok
37
38
39def check_submodules():
40 """Iterates through all submodules to make sure they're cloned and up to date.
41 """
42 ok = True
43
44 for submodule in submodules.status().values():
45 if submodule['status'] is None:
46 if submodule['name'] in ESSENTIAL_SUBMODULES:
47 cli.log.error('Submodule %s has not yet been cloned!', submodule['name'])
48 ok = False
49 else:
50 cli.log.warn('Submodule %s is not available.', submodule['name'])
51 elif not submodule['status']:
52 if submodule['name'] in ESSENTIAL_SUBMODULES:
53 cli.log.warn('Submodule %s is not up to date!')
54
55 return ok
56
57
58def check_udev_rules():
59 """Make sure the udev rules look good.
60 """
61 ok = True
62 udev_dir = Path("/etc/udev/rules.d/")
63 desired_rules = {
64 'dfu': {_udev_rule("03eb", "2ff4"), _udev_rule("03eb", "2ffb"), _udev_rule("03eb", "2ff0")},
65 'tmk': {_udev_rule("feed")},
66 'input_club': {_udev_rule("1c11")},
67 'stm32': {_udev_rule("1eaf", "0003"), _udev_rule("0483", "df11")},
68 'caterina': {'ATTRS{idVendor}=="2a03", ENV{ID_MM_DEVICE_IGNORE}="1"', 'ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"'},
69 }
70
71 if udev_dir.exists():
72 udev_rules = [str(rule_file) for rule_file in udev_dir.glob('*.rules')]
73 current_rules = set()
74
75 # Collect all rules from the config files
76 for rule_file in udev_rules:
77 with open(rule_file, "r") as fd:
78 for line in fd.readlines():
79 line = line.strip()
80 if not line.startswith("#") and len(line):
81 current_rules.add(line)
82
83 # Check if the desired rules are among the currently present rules
84 for bootloader, rules in desired_rules.items():
85 if not rules.issubset(current_rules):
86 # If the rules for catalina are not present, check if ModemManager is running
87 if bootloader == "caterina":
88 if check_modem_manager():
89 ok = False
90 cli.log.warn("{bg_yellow}Detected ModemManager without udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro.")
91 else:
92 cli.log.warn("{bg_yellow}Missing udev rules for '%s' boards. You'll need to use `sudo` in order to flash them.", bootloader)
93
94 return ok
95
96
97def check_modem_manager():
98 """Returns True if ModemManager is running.
99 """
100 if shutil.which("systemctl"):
101 mm_check = subprocess.run(["systemctl", "--quiet", "is-active", "ModemManager.service"], timeout=10)
102 if mm_check.returncode == 0:
103 return True
104
105 else:
106 cli.log.warn("Can't find systemctl to check for ModemManager.")
107
108
109def is_executable(command):
110 """Returns True if command exists and can be executed.
111 """
112 # Make sure the command is in the path.
113 res = shutil.which(command)
114 if res is None:
115 cli.log.error("{fg_red}Can't find %s in your path.", command)
116 return False
117
118 # Make sure the command can be executed
119 check = subprocess.run([command, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
120 if check.returncode in [0, 1]: # Older versions of dfu-programmer exit 1
121 cli.log.debug('Found {fg_cyan}%s', command)
122 return True
123
124 cli.log.error("{fg_red}Can't run `%s --version`", command)
125 return False
126
127
128def os_test_linux():
129 """Run the Linux specific tests.
130 """
131 cli.log.info("Detected {fg_cyan}Linux.")
132 ok = True
133
134 if not check_udev_rules():
135 ok = False
136
137 return ok
138
139
140def os_test_macos():
141 """Run the Mac specific tests.
142 """
143 cli.log.info("Detected {fg_cyan}macOS.")
144
145 return True
146
147
148def os_test_windows():
149 """Run the Windows specific tests.
150 """
151 cli.log.info("Detected {fg_cyan}Windows.")
152
153 return True
154
155
156@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
157@cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.')
23@cli.subcommand('Basic QMK environment checks') 158@cli.subcommand('Basic QMK environment checks')
24def doctor(cli): 159def doctor(cli):
25 """Basic QMK environment checks. 160 """Basic QMK environment checks.
@@ -30,75 +165,53 @@ def doctor(cli):
30 * [ ] Compile a trivial program with each compiler 165 * [ ] Compile a trivial program with each compiler
31 """ 166 """
32 cli.log.info('QMK Doctor is checking your environment.') 167 cli.log.info('QMK Doctor is checking your environment.')
33
34 # Make sure the basic CLI tools we need are available and can be executed.
35 binaries = ['dfu-programmer', 'avrdude', 'dfu-util', 'avr-gcc', 'arm-none-eabi-gcc', 'bin/qmk']
36 ok = True 168 ok = True
37 169
38 for binary in binaries:
39 res = shutil.which(binary)
40 if res is None:
41 cli.log.error("{fg_red}QMK can't find %s in your path.", binary)
42 ok = False
43 else:
44 check = subprocess.run([binary, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
45 if check.returncode in [0, 1]:
46 cli.log.info('Found {fg_cyan}%s', binary)
47 else:
48 cli.log.error("{fg_red}Can't run `%s --version`", binary)
49 ok = False
50
51 # Determine our OS and run platform specific tests 170 # Determine our OS and run platform specific tests
52 OS = platform.system() # noqa (N806), uppercase name is ok in this instance 171 OS = platform.system() # noqa (N806), uppercase name is ok in this instance
53 172
54 if OS == "Darwin": 173 if OS == 'Darwin':
55 cli.log.info("Detected {fg_cyan}macOS.") 174 if not os_test_macos():
56 175 ok = False
57 elif OS == "Linux": 176 elif OS == 'Linux':
58 cli.log.info("Detected {fg_cyan}Linux.") 177 if not os_test_linux():
59 # Checking for udev rules 178 ok = False
60 udev_dir = "/etc/udev/rules.d/" 179 elif OS == 'Windows':
61 # These are the recommended udev rules 180 if not os_test_windows():
62 desired_rules = { 181 ok = False
63 'dfu': {_udev_rule("03eb", "2ff4"), _udev_rule("03eb", "2ffb"), _udev_rule("03eb", "2ff0")}, 182 else:
64 'tmk': {_udev_rule("feed")}, 183 cli.log.error('Unsupported OS detected: %s', OS)
65 'input_club': {_udev_rule("1c11")}, 184 ok = False
66 'stm32': {_udev_rule("1eaf", "0003"), _udev_rule("0483", "df11")},
67 'caterina': {'ATTRS{idVendor}=="2a03", ENV{ID_MM_DEVICE_IGNORE}="1"', 'ATTRS{idVendor}=="2341", ENV{ID_MM_DEVICE_IGNORE}="1"'},
68 }
69
70 if os.path.exists(udev_dir):
71 udev_rules = [rule for rule in glob.iglob(os.path.join(udev_dir, "*.rules")) if os.path.isfile(rule)]
72 # Collect all rules from the config files
73 current_rules = set()
74 for rule in udev_rules:
75 with open(rule, "r") as fd:
76 for line in fd.readlines():
77 line = line.strip()
78 if not line.startswith("#") and len(line):
79 current_rules.add(line)
80
81 # Check if the desired rules are among the currently present rules
82 for bootloader, rules in desired_rules.items():
83 if not rules.issubset(current_rules):
84 # If the rules for catalina are not present, check if ModemManager is running
85 if bootloader == "caterina":
86 if shutil.which("systemctl"):
87 mm_check = subprocess.run(["systemctl", "--quiet", "is-active", "ModemManager.service"], timeout=10)
88 if mm_check.returncode == 0:
89 ok = False
90 cli.log.warn("{bg_yellow}Detected ModemManager without udev rules. Please either disable it or set the appropriate udev rules if you are using a Pro Micro.")
91 else:
92 cli.log.warn("Can't find systemctl to check for ModemManager.")
93 else:
94 cli.log.warn("{bg_yellow}Missing udev rules for '%s' boards. You'll need to use `sudo` in order to flash them.", bootloader)
95 185
186 # Make sure the basic CLI tools we need are available and can be executed.
187 bin_ok = check_binaries()
188
189 if not bin_ok:
190 if yesno('Would you like to install dependencies?', default=True):
191 subprocess.run(['util/qmk_install.sh'])
192 bin_ok = check_binaries()
193
194 if bin_ok:
195 cli.log.info('All dependencies are installed.')
96 else: 196 else:
97 cli.log.info("Assuming {fg_cyan}Windows.") 197 ok = False
198
199 # Check out the QMK submodules
200 sub_ok = check_submodules()
201
202 if sub_ok:
203 cli.log.info('Submodules are up to date.')
204 else:
205 if yesno('Would you like to clone the submodules?', default=True):
206 submodules.update()
207 sub_ok = check_submodules()
208
209 if not sub_ok:
210 ok = False
98 211
99 # Report a summary of our findings to the user 212 # Report a summary of our findings to the user
100 if ok: 213 if ok:
101 cli.log.info('{fg_green}QMK is ready to go') 214 cli.log.info('{fg_green}QMK is ready to go')
102 else: 215 else:
103 cli.log.info('{fg_yellow}Problems detected, please fix these problems before proceeding.') 216 cli.log.info('{fg_yellow}Problems detected, please fix these problems before proceeding.')
104 # FIXME(skullydazed): Link to a document about troubleshooting, or discord or something 217 # FIXME(skullydazed/unclaimed): Link to a document about troubleshooting, or discord or something
diff --git a/lib/python/qmk/questions.py b/lib/python/qmk/questions.py
new file mode 100644
index 000000000..34b0b43bc
--- /dev/null
+++ b/lib/python/qmk/questions.py
@@ -0,0 +1,97 @@
1"""Functions to collect user input.
2"""
3
4from milc import cli, format_ansi
5
6
7def yesno(prompt, *args, default=None, **kwargs):
8 """Displays prompt to the user and gets a yes or no response.
9
10 Returns True for a yes and False for a no.
11
12 If you add `--yes` and `--no` arguments to your program the user can answer questions by passing command line flags.
13
14 @add_argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
15 @add_argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.')
16
17 Arguments:
18 prompt
19 The prompt to present to the user. Can include ANSI and format strings like milc's `cli.print()`.
20
21 default
22 Whether to default to a Yes or No when the user presses enter.
23
24 None- force the user to enter Y or N
25
26 True- Default to yes
27
28 False- Default to no
29 """
30 if not args and kwargs:
31 args = kwargs
32
33 if 'no' in cli.args and cli.args.no:
34 return False
35
36 if 'yes' in cli.args and cli.args.yes:
37 return True
38
39 if default is not None:
40 if default:
41 prompt = prompt + ' [Y/n] '
42 else:
43 prompt = prompt + ' [y/N] '
44
45 while True:
46 print()
47 answer = input(format_ansi(prompt % args))
48 print()
49
50 if not answer and prompt is not None:
51 return default
52
53 elif answer.lower() in ['y', 'yes']:
54 return True
55
56 elif answer.lower() in ['n', 'no']:
57 return False
58
59
60def question(prompt, *args, default=None, confirm=False, answer_type=str, **kwargs):
61 """Prompt the user to answer a question with a free-form input.
62
63 prompt
64 The prompt to present to the user. Can include ANSI and format strings like milc's `cli.print()`.
65
66 default
67 The value to return when the user doesn't enter any value. Use None to prompt until they enter a value.
68
69 answer_type
70 Specify a type function for the answer. Will re-prompt the user if the function raises any errors. Common choices here include int, float, and decimal.Decimal.
71 """
72 if not args and kwargs:
73 args = kwargs
74
75 if default is not None:
76 prompt = '%s [%s] ' % (prompt, default)
77
78 while True:
79 print()
80 answer = input(format_ansi(prompt % args))
81 print()
82
83 if answer:
84 if confirm:
85 if yesno('Is the answer "%s" correct?', answer, default=True):
86 try:
87 return answer_type(answer)
88 except Exception as e:
89 cli.log.error('Could not convert answer (%s) to type %s: %s', answer, answer_type.__name__, str(e))
90 else:
91 try:
92 return answer_type(answer)
93 except Exception as e:
94 cli.log.error('Could not convert answer (%s) to type %s: %s', answer, answer_type.__name__, str(e))
95
96 elif default is not None:
97 return default
diff --git a/lib/python/qmk/submodules.py b/lib/python/qmk/submodules.py
new file mode 100644
index 000000000..be51a6804
--- /dev/null
+++ b/lib/python/qmk/submodules.py
@@ -0,0 +1,71 @@
1"""Functions for working with QMK's submodules.
2"""
3
4import subprocess
5
6
7def status():
8 """Returns a dictionary of submodules.
9
10 Each entry is a dict of the form:
11
12 {
13 'name': 'submodule_name',
14 'status': None/False/True,
15 'githash': '<sha-1 hash for the submodule>
16 }
17
18 status is None when the submodule doesn't exist, False when it's out of date, and True when it's current
19 """
20 submodules = {}
21 git_cmd = subprocess.run(['git', 'submodule', 'status'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30, universal_newlines=True)
22
23 for line in git_cmd.stdout.split('\n'):
24 if not line:
25 continue
26
27 status = line[0]
28 githash, submodule = line[1:].split()[:2]
29 submodules[submodule] = {'name': submodule, 'githash': githash}
30
31 if status == '-':
32 submodules[submodule]['status'] = None
33 elif status == '+':
34 submodules[submodule]['status'] = False
35 elif status == ' ':
36 submodules[submodule]['status'] = True
37 else:
38 raise ValueError('Unknown `git submodule status` sha-1 prefix character: "%s"' % status)
39
40 return submodules
41
42
43def update(submodules=None):
44 """Update the submodules.
45
46 submodules
47 A string containing a single submodule or a list of submodules.
48 """
49 git_sync_cmd = ['git', 'submodule', 'sync']
50 git_update_cmd = ['git', 'submodule', 'update', '--init']
51
52 if submodules is None:
53 # Update everything
54 git_sync_cmd.append('--recursive')
55 git_update_cmd.append('--recursive')
56 subprocess.run(git_sync_cmd, check=True)
57 subprocess.run(git_update_cmd, check=True)
58
59 else:
60 if isinstance(submodules, str):
61 # Update only a single submodule
62 git_sync_cmd.append(submodules)
63 git_update_cmd.append(submodules)
64 subprocess.run(git_sync_cmd, check=True)
65 subprocess.run(git_update_cmd, check=True)
66
67 else:
68 # Update submodules in a list
69 for submodule in submodules:
70 subprocess.run(git_sync_cmd + [submodule], check=True)
71 subprocess.run(git_update_cmd + [submodule], check=True)