qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

main.py (10163B)


      1 """QMK Doctor
      2 
      3 Check out the user's QMK environment and make sure it's ready to compile.
      4 """
      5 import platform
      6 
      7 from milc import cli
      8 from milc.questions import yesno
      9 
     10 from qmk import submodules
     11 from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM, QMK_USERSPACE, HAS_QMK_USERSPACE
     12 from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
     13 from 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
     14 from qmk.commands import in_virtualenv
     15 from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError
     16 
     17 
     18 def 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 
     72 def 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 
     91 def 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 
    131 def 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 
    149 def 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')
    171 def 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