at master 10 kB view raw
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