keyboard stuff
1"""QMK Doctor
2
3Check out the user's QMK environment and make sure it's ready to compile.
4"""
5import platform
6
7from milc import cli
8from milc.questions import yesno
9
10from qmk import submodules
11from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM, QMK_USERSPACE, HAS_QMK_USERSPACE
12from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
13from qmk.git import git_check_repo, git_get_branch, git_get_tag, git_get_last_log_entry, git_get_common_ancestor, git_is_dirty, git_get_remotes, git_check_deviation
14from qmk.commands import in_virtualenv
15from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError
16
17
18def distrib_tests():
19 def _load_kvp_file(file):
20 """Load a simple key=value file into a dictionary
21 """
22 vars = {}
23 with open(file, 'r') as f:
24 for line in f:
25 if '=' in line:
26 key, value = line.split('=', 1)
27 vars[key.strip()] = value.strip()
28 return vars
29
30 def _parse_toolchain_release_file(file):
31 """Parse the QMK toolchain release info file
32 """
33 try:
34 vars = _load_kvp_file(file)
35 return f'{vars.get("TOOLCHAIN_HOST", "unknown")}:{vars.get("TOOLCHAIN_TARGET", "unknown")}:{vars.get("COMMIT_HASH", "unknown")}'
36 except Exception as e:
37 cli.log.warning('Error reading QMK toolchain release info file: %s', e)
38 return f'Unknown toolchain release info file: {file}'
39
40 def _parse_flashutils_release_file(file):
41 """Parse the QMK flashutils release info file
42 """
43 try:
44 vars = _load_kvp_file(file)
45 return f'{vars.get("FLASHUTILS_HOST", "unknown")}:{vars.get("COMMIT_HASH", "unknown")}'
46 except Exception as e:
47 cli.log.warning('Error reading QMK flashutils release info file: %s', e)
48 return f'Unknown flashutils release info file: {file}'
49
50 try:
51 from qmk.cli import QMK_DISTRIB_DIR
52 if (QMK_DISTRIB_DIR / 'etc').exists():
53 cli.log.info('Found QMK tools distribution directory: {fg_cyan}%s', QMK_DISTRIB_DIR)
54
55 toolchains = [_parse_toolchain_release_file(file) for file in (QMK_DISTRIB_DIR / 'etc').glob('toolchain_release_*')]
56 if len(toolchains) > 0:
57 cli.log.info('Found QMK toolchains: {fg_cyan}%s', ', '.join(toolchains))
58 else:
59 cli.log.warning('No QMK toolchains manifest found.')
60
61 flashutils = [_parse_flashutils_release_file(file) for file in (QMK_DISTRIB_DIR / 'etc').glob('flashutils_release_*')]
62 if len(flashutils) > 0:
63 cli.log.info('Found QMK flashutils: {fg_cyan}%s', ', '.join(flashutils))
64 else:
65 cli.log.warning('No QMK flashutils manifest found.')
66 except ImportError:
67 cli.log.info('QMK tools distribution not found.')
68
69 return CheckStatus.OK
70
71
72def os_tests():
73 """Determine our OS and run platform specific tests
74 """
75 platform_id = platform.platform().lower()
76
77 if 'darwin' in platform_id or 'macos' in platform_id:
78 from .macos import os_test_macos
79 return os_test_macos()
80 elif 'linux' in platform_id:
81 from .linux import os_test_linux
82 return os_test_linux()
83 elif 'windows' in platform_id:
84 from .windows import os_test_windows
85 return os_test_windows()
86 else:
87 cli.log.warning('Unsupported OS detected: %s', platform_id)
88 return CheckStatus.WARNING
89
90
91def git_tests():
92 """Run Git-related checks
93 """
94 status = CheckStatus.OK
95
96 # Make sure our QMK home is a Git repo
97 git_ok = git_check_repo()
98 if not git_ok:
99 cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)")
100 status = CheckStatus.WARNING
101 else:
102 git_branch = git_get_branch()
103 if git_branch:
104 cli.log.info('Git branch: %s', git_branch)
105
106 repo_version = git_get_tag()
107 if repo_version:
108 cli.log.info('Repo version: %s', repo_version)
109
110 git_dirty = git_is_dirty()
111 if git_dirty:
112 cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')
113 status = CheckStatus.WARNING
114 git_remotes = git_get_remotes()
115 if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''):
116 cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".')
117 status = CheckStatus.WARNING
118 else:
119 git_deviation = git_check_deviation(git_branch)
120 if git_branch in ['master', 'develop'] and git_deviation:
121 cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch)
122 status = CheckStatus.WARNING
123 for branch in [git_branch, 'upstream/master', 'upstream/develop']:
124 cli.log.info('- Latest %s: %s', branch, git_get_last_log_entry(branch))
125 for branch in ['upstream/master', 'upstream/develop']:
126 cli.log.info('- Common ancestor with %s: %s', branch, git_get_common_ancestor(branch, 'HEAD'))
127
128 return status
129
130
131def output_submodule_status():
132 """Prints out information related to the submodule status.
133 """
134 cli.log.info('Submodule status:')
135 sub_status = submodules.status()
136 for s in sub_status.keys():
137 sub_info = sub_status[s]
138 if 'name' in sub_info:
139 sub_name = sub_info['name']
140 sub_shorthash = sub_info['shorthash'] if 'shorthash' in sub_info else ''
141 sub_describe = sub_info['describe'] if 'describe' in sub_info else ''
142 sub_last_log_timestamp = sub_info['last_log_timestamp'] if 'last_log_timestamp' in sub_info else ''
143 if sub_last_log_timestamp != '':
144 cli.log.info(f'- {sub_name}: {sub_last_log_timestamp} -- {sub_describe} ({sub_shorthash})')
145 else:
146 cli.log.error(f'- {sub_name}: <<< missing or unknown >>>')
147
148
149def userspace_tests(qmk_firmware):
150 if qmk_firmware:
151 cli.log.info(f'QMK home: {{fg_cyan}}{qmk_firmware}')
152
153 for path in qmk_userspace_paths():
154 try:
155 qmk_userspace_validate(path)
156 cli.log.info(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_green}}Valid `qmk.json`')
157 except FileNotFoundError:
158 cli.log.warning(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Missing `qmk.json`')
159 except UserspaceValidationError as err:
160 cli.log.warning(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Invalid `qmk.json`')
161 cli.log.warning(f' -- {{fg_cyan}}{path}/qmk.json{{fg_reset}} validation error: {err}')
162
163 if QMK_USERSPACE is not None:
164 cli.log.info(f'QMK userspace: {{fg_cyan}}{QMK_USERSPACE}')
165 cli.log.info(f'Userspace enabled: {{fg_cyan}}{HAS_QMK_USERSPACE}')
166
167
168@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
169@cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.')
170@cli.subcommand('Basic QMK environment checks')
171def doctor(cli):
172 """Basic QMK environment checks.
173
174 This is currently very simple, it just checks that all the expected binaries are on your system.
175
176 TODO(unclaimed):
177 * [ ] Compile a trivial program with each compiler
178 """
179 cli.log.info('QMK Doctor is checking your environment.')
180 cli.log.info('Python version: %s', platform.python_version())
181 cli.log.info('CLI version: %s', cli.version)
182 cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE)
183
184 status = os_status = os_tests()
185 distrib_tests()
186
187 userspace_tests(None)
188
189 git_status = git_tests()
190
191 if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING):
192 status = git_status
193
194 if in_virtualenv():
195 cli.log.info('CLI installed in virtualenv.')
196
197 # Make sure the basic CLI tools we need are available and can be executed.
198 bin_ok = check_binaries()
199 if bin_ok == CheckStatus.OK:
200 cli.log.info('All dependencies are installed.')
201 elif bin_ok == CheckStatus.WARNING:
202 cli.log.warning('Issues encountered while checking dependencies.')
203 else:
204 status = CheckStatus.ERROR
205
206 # Make sure the tools are at the correct version
207 ver_ok = check_binary_versions()
208 if CheckStatus.ERROR in ver_ok:
209 status = CheckStatus.ERROR
210 elif CheckStatus.WARNING in ver_ok and status == CheckStatus.OK:
211 status = CheckStatus.WARNING
212
213 # Check out the QMK submodules
214 sub_ok = check_submodules()
215 if sub_ok == CheckStatus.OK:
216 cli.log.info('Submodules are up to date.')
217 else:
218 if git_check_repo() and yesno('Would you like to clone the submodules?', default=True):
219 submodules.update()
220 sub_ok = check_submodules()
221
222 if sub_ok == CheckStatus.ERROR:
223 status = CheckStatus.ERROR
224 elif sub_ok == CheckStatus.WARNING and status == CheckStatus.OK:
225 status = CheckStatus.WARNING
226
227 output_submodule_status()
228
229 # Report a summary of our findings to the user
230 if status == CheckStatus.OK:
231 cli.log.info('{fg_green}QMK is ready to go')
232 return 0
233 elif status == CheckStatus.WARNING:
234 cli.log.info('{fg_yellow}QMK is ready to go, but minor problems were found')
235 return 1
236 else:
237 cli.log.info('{fg_red}Major problems detected, please fix these problems before proceeding.{fg_reset}')
238 cli.log.info('{fg_blue}If you\'re missing dependencies, try following the instructions on: https://docs.qmk.fm/newbs_getting_started{fg_reset}')
239 cli.log.info('{fg_blue}Additionally, check out the FAQ (https://docs.qmk.fm/#/faq_build) or join the QMK Discord (https://discord.gg/qmk) for help.{fg_reset}')
240 return 2