summaryrefslogtreecommitdiff
path: root/lib/python/qmk/cli/doctor/main.py
blob: dd8b58b2c757ff4c0b0111cc62fb0c8075f1a8e6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""QMK Doctor

Check out the user's QMK environment and make sure it's ready to compile.
"""
import platform
from subprocess import DEVNULL

from milc import cli
from milc.questions import yesno

from qmk import submodules
from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM, QMK_USERSPACE, HAS_QMK_USERSPACE
from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules
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
from qmk.commands import in_virtualenv
from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError


def os_tests():
    """Determine our OS and run platform specific tests
    """
    platform_id = platform.platform().lower()

    if 'darwin' in platform_id or 'macos' in platform_id:
        from .macos import os_test_macos
        return os_test_macos()
    elif 'linux' in platform_id:
        from .linux import os_test_linux
        return os_test_linux()
    elif 'windows' in platform_id:
        from .windows import os_test_windows
        return os_test_windows()
    else:
        cli.log.warning('Unsupported OS detected: %s', platform_id)
        return CheckStatus.WARNING


def git_tests():
    """Run Git-related checks
    """
    status = CheckStatus.OK

    # Make sure our QMK home is a Git repo
    git_ok = git_check_repo()
    if not git_ok:
        cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)")
        status = CheckStatus.WARNING
    else:
        git_branch = git_get_branch()
        if git_branch:
            cli.log.info('Git branch: %s', git_branch)

            repo_version = git_get_tag()
            if repo_version:
                cli.log.info('Repo version: %s', repo_version)

            git_dirty = git_is_dirty()
            if git_dirty:
                cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.')
                status = CheckStatus.WARNING
            git_remotes = git_get_remotes()
            if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''):
                cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".')
                status = CheckStatus.WARNING
            else:
                git_deviation = git_check_deviation(git_branch)
                if git_branch in ['master', 'develop'] and git_deviation:
                    cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch)
                    status = CheckStatus.WARNING
                for branch in [git_branch, 'upstream/master', 'upstream/develop']:
                    cli.log.info('- Latest %s: %s', branch, git_get_last_log_entry(branch))
                for branch in ['upstream/master', 'upstream/develop']:
                    cli.log.info('- Common ancestor with %s: %s', branch, git_get_common_ancestor(branch, 'HEAD'))

    return status


def output_submodule_status():
    """Prints out information related to the submodule status.
    """
    cli.log.info('Submodule status:')
    sub_status = submodules.status()
    for s in sub_status.keys():
        sub_info = sub_status[s]
        if 'name' in sub_info:
            sub_name = sub_info['name']
            sub_shorthash = sub_info['shorthash'] if 'shorthash' in sub_info else ''
            sub_describe = sub_info['describe'] if 'describe' in sub_info else ''
            sub_last_log_timestamp = sub_info['last_log_timestamp'] if 'last_log_timestamp' in sub_info else ''
            if sub_last_log_timestamp != '':
                cli.log.info(f'- {sub_name}: {sub_last_log_timestamp} -- {sub_describe} ({sub_shorthash})')
            else:
                cli.log.error(f'- {sub_name}: <<< missing or unknown >>>')


def userspace_tests(qmk_firmware):
    if qmk_firmware:
        cli.log.info(f'QMK home: {{fg_cyan}}{qmk_firmware}')

    for path in qmk_userspace_paths():
        try:
            qmk_userspace_validate(path)
            cli.log.info(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_green}}Valid `qmk.json`')
        except FileNotFoundError:
            cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Missing `qmk.json`')
        except UserspaceValidationError as err:
            cli.log.warn(f'Testing userspace candidate: {{fg_cyan}}{path}{{fg_reset}} -- {{fg_red}}Invalid `qmk.json`')
            cli.log.warn(f' -- {{fg_cyan}}{path}/qmk.json{{fg_reset}} validation error: {err}')

    if QMK_USERSPACE is not None:
        cli.log.info(f'QMK userspace: {{fg_cyan}}{QMK_USERSPACE}')
    cli.log.info(f'Userspace enabled: {{fg_cyan}}{HAS_QMK_USERSPACE}')


@cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.')
@cli.argument('-n', '--no', action='store_true', arg_only=True, help='Answer no to all questions.')
@cli.subcommand('Basic QMK environment checks')
def doctor(cli):
    """Basic QMK environment checks.

    This is currently very simple, it just checks that all the expected binaries are on your system.

    TODO(unclaimed):
        * [ ] Compile a trivial program with each compiler
    """
    cli.log.info('QMK Doctor is checking your environment.')
    cli.log.info('CLI version: %s', cli.version)
    cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE)

    status = os_status = os_tests()

    userspace_tests(None)

    git_status = git_tests()

    if git_status == CheckStatus.ERROR or (os_status == CheckStatus.OK and git_status == CheckStatus.WARNING):
        status = git_status

    if in_virtualenv():
        cli.log.info('CLI installed in virtualenv.')

    # Make sure the basic CLI tools we need are available and can be executed.
    bin_ok = check_binaries()

    if bin_ok == CheckStatus.ERROR:
        if yesno('Would you like to install dependencies?', default=True):
            cli.run(['util/qmk_install.sh', '-y'], stdin=DEVNULL, capture_output=False)
            bin_ok = check_binaries()

    if bin_ok == CheckStatus.OK:
        cli.log.info('All dependencies are installed.')
    elif bin_ok == CheckStatus.WARNING:
        cli.log.warning('Issues encountered while checking dependencies.')
    else:
        status = CheckStatus.ERROR

    # Make sure the tools are at the correct version
    ver_ok = check_binary_versions()
    if CheckStatus.ERROR in ver_ok:
        status = CheckStatus.ERROR
    elif CheckStatus.WARNING in ver_ok and status == CheckStatus.OK:
        status = CheckStatus.WARNING

    # Check out the QMK submodules
    sub_ok = check_submodules()

    if sub_ok == CheckStatus.OK:
        cli.log.info('Submodules are up to date.')
    else:
        if git_check_repo() and yesno('Would you like to clone the submodules?', default=True):
            submodules.update()
            sub_ok = check_submodules()

        if sub_ok == CheckStatus.ERROR:
            status = CheckStatus.ERROR
        elif sub_ok == CheckStatus.WARNING and status == CheckStatus.OK:
            status = CheckStatus.WARNING

    output_submodule_status()

    # Report a summary of our findings to the user
    if status == CheckStatus.OK:
        cli.log.info('{fg_green}QMK is ready to go')
        return 0
    elif status == CheckStatus.WARNING:
        cli.log.info('{fg_yellow}QMK is ready to go, but minor problems were found')
        return 1
    else:
        cli.log.info('{fg_red}Major problems detected, please fix these problems before proceeding.')
        cli.log.info('{fg_blue}Check out the FAQ (https://docs.qmk.fm/#/faq_build) or join the QMK Discord (https://discord.gg/Uq7gcHh) for help.')
        return 2