summaryrefslogtreecommitdiffstats
path: root/lib/python/qmk/cli/__init__.py
blob: 3face93a535b3b31558835d22cc7614f5d6c376c (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
"""QMK CLI Subcommands

We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
"""
import os
import shlex
import sys
from importlib.util import find_spec
from pathlib import Path
from subprocess import run

from milc import cli, __VERSION__
from milc.questions import yesno


import_names = {
    # A mapping of package name to importable name
    'pep8-naming': 'pep8ext_naming',
    'pyusb': 'usb.core',
}

safe_commands = [
    # A list of subcommands we always run, even when the module imports fail
    'clone',
    'config',
    'env',
    'setup',
]


def _run_cmd(*command):
    """Run a command in a subshell.
    """
    if 'windows' in cli.platform.lower():
        safecmd = map(shlex.quote, command)
        safecmd = ' '.join(safecmd)
        command = [os.environ['SHELL'], '-c', safecmd]

    return run(command)


def _find_broken_requirements(requirements):
    """ Check if the modules in the given requirements.txt are available.

    Args:

        requirements
            The path to a requirements.txt file

    Returns a list of modules that couldn't be imported
    """
    with Path(requirements).open() as fd:
        broken_modules = []

        for line in fd.readlines():
            line = line.strip().replace('<', '=').replace('>', '=')

            if len(line) == 0 or line[0] == '#' or line.startswith('-r'):
                continue

            if '#' in line:
                line = line.split('#')[0]

            module_name = line.split('=')[0] if '=' in line else line
            module_import = module_name.replace('-', '_')

            # Not every module is importable by its own name.
            if module_name in import_names:
                module_import = import_names[module_name]

            if not find_spec(module_import):
                broken_modules.append(module_name)

        return broken_modules


def _broken_module_imports(requirements):
    """Make sure we can import all the python modules.
    """
    broken_modules = _find_broken_requirements(requirements)

    for module in broken_modules:
        print('Could not find module %s!' % module)

    if broken_modules:
        return True

    return False


# Make sure our python is new enough
#
# Supported version information
#
# Based on the OSes we support these are the minimum python version available by default.
# Last update: 2021 Jan 02
#
# Arch: 3.9
# Debian: 3.7
# Fedora 31: 3.7
# Fedora 32: 3.8
# Fedora 33: 3.9
# FreeBSD: 3.7
# Gentoo: 3.7
# macOS: 3.9 (from homebrew)
# msys2: 3.8
# Slackware: 3.7
# solus: 3.7
# void: 3.9

if sys.version_info[0] != 3 or sys.version_info[1] < 7:
    print('Error: Your Python is too old! Please upgrade to Python 3.7 or later.')
    exit(127)

milc_version = __VERSION__.split('.')

if int(milc_version[0]) < 2 and int(milc_version[1]) < 3:
    requirements = Path('requirements.txt').resolve()

    print(f'Your MILC library is too old! Please upgrade: python3 -m pip install -U -r {str(requirements)}')
    exit(127)

# Check to make sure we have all our dependencies
msg_install = 'Please run `python3 -m pip install -r %s` to install required python dependencies.'
args = sys.argv[1:]
while args and args[0][0] == '-':
    del args[0]

if not args or args[0] not in safe_commands:
    if _broken_module_imports('requirements.txt'):
        if yesno('Would you like to install the required Python modules?'):
            _run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt')
        else:
            print()
            print(msg_install % (str(Path('requirements.txt').resolve()),))
            print()
            exit(1)

    if cli.config.user.developer and _broken_module_imports('requirements-dev.txt'):
        if yesno('Would you like to install the required developer Python modules?'):
            _run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements-dev.txt')
        elif yesno('Would you like to disable developer mode?'):
            _run_cmd(sys.argv[0], 'config', 'user.developer=None')
        else:
            print()
            print(msg_install % (str(Path('requirements-dev.txt').resolve()),))
            print('You can also turn off developer mode: qmk config user.developer=None')
            print()
            exit(1)

# Import our subcommands
from . import c2json  # noqa
from . import cformat  # noqa
from . import chibios  # noqa
from . import clean  # noqa
from . import compile  # noqa
from . import config  # noqa
from . import console  # noqa
from . import docs  # noqa
from . import doctor  # noqa
from . import fileformat  # noqa
from . import flash  # noqa
from . import format  # noqa
from . import generate  # noqa
from . import hello  # noqa
from . import info  # noqa
from . import json2c  # noqa
from . import lint  # noqa
from . import list  # noqa
from . import kle2json  # noqa
from . import multibuild  # noqa
from . import new  # noqa
from . import pyformat  # noqa
from . import pytest  # noqa