at master 8.5 kB view raw
1"""QMK CLI Subcommands 2 3We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup. 4""" 5import os 6import platform 7import platformdirs 8import shlex 9import sys 10from importlib.util import find_spec 11from pathlib import Path 12from subprocess import run 13 14from milc import cli, __VERSION__ 15from milc.questions import yesno 16 17 18def _get_default_distrib_path(): 19 if 'windows' in platform.platform().lower(): 20 try: 21 result = cli.run(['cygpath', '-w', '/opt/qmk']) 22 if result.returncode == 0: 23 return result.stdout.strip() 24 except Exception: 25 pass 26 27 return platformdirs.user_data_dir('qmk') 28 29 30# Ensure the QMK distribution is on the `$PATH` if present. This must be kept in sync with qmk/qmk_cli. 31QMK_DISTRIB_DIR = Path(os.environ.get('QMK_DISTRIB_DIR', _get_default_distrib_path())) 32if QMK_DISTRIB_DIR.exists(): 33 os.environ['PATH'] = str(QMK_DISTRIB_DIR / 'bin') + os.pathsep + os.environ['PATH'] 34 35# Prepend any user-defined path prefix 36if 'QMK_PATH_PREFIX' in os.environ: 37 os.environ['PATH'] = os.environ['QMK_PATH_PREFIX'] + os.pathsep + os.environ['PATH'] 38 39import_names = { 40 # A mapping of package name to importable name 41 'pep8-naming': 'pep8ext_naming', 42 'pyserial': 'serial', 43 'pyusb': 'usb.core', 44 'qmk-dotty-dict': 'dotty_dict', 45 'pillow': 'PIL' 46} 47 48safe_commands = [ 49 # A list of subcommands we always run, even when the module imports fail 50 'clone', 51 'config', 52 'doctor', 53 'env', 54 'setup', 55] 56 57subcommands = [ 58 'qmk.cli.ci.validate_aliases', 59 'qmk.cli.bux', 60 'qmk.cli.c2json', 61 'qmk.cli.cd', 62 'qmk.cli.chibios.confmigrate', 63 'qmk.cli.clean', 64 'qmk.cli.compile', 65 'qmk.cli.docs', 66 'qmk.cli.doctor', 67 'qmk.cli.find', 68 'qmk.cli.flash', 69 'qmk.cli.format.c', 70 'qmk.cli.format.json', 71 'qmk.cli.format.python', 72 'qmk.cli.format.text', 73 'qmk.cli.generate.api', 74 'qmk.cli.generate.autocorrect_data', 75 'qmk.cli.generate.compilation_database', 76 'qmk.cli.generate.community_modules', 77 'qmk.cli.generate.config_h', 78 'qmk.cli.generate.develop_pr_list', 79 'qmk.cli.generate.dfu_header', 80 'qmk.cli.generate.docs', 81 'qmk.cli.generate.info_json', 82 'qmk.cli.generate.keyboard_c', 83 'qmk.cli.generate.keyboard_h', 84 'qmk.cli.generate.keycodes', 85 'qmk.cli.generate.keymap_h', 86 'qmk.cli.generate.make_dependencies', 87 'qmk.cli.generate.rgb_breathe_table', 88 'qmk.cli.generate.rules_mk', 89 'qmk.cli.generate.version_h', 90 'qmk.cli.git.submodule', 91 'qmk.cli.hello', 92 'qmk.cli.import.kbfirmware', 93 'qmk.cli.import.keyboard', 94 'qmk.cli.import.keymap', 95 'qmk.cli.info', 96 'qmk.cli.json2c', 97 'qmk.cli.license_check', 98 'qmk.cli.lint', 99 'qmk.cli.kle2json', 100 'qmk.cli.list.keyboards', 101 'qmk.cli.list.keymaps', 102 'qmk.cli.list.layouts', 103 'qmk.cli.mass_compile', 104 'qmk.cli.migrate', 105 'qmk.cli.new.keyboard', 106 'qmk.cli.new.keymap', 107 'qmk.cli.painter', 108 'qmk.cli.pytest', 109 'qmk.cli.resolve_alias', 110 'qmk.cli.test.c', 111 'qmk.cli.userspace.add', 112 'qmk.cli.userspace.compile', 113 'qmk.cli.userspace.doctor', 114 'qmk.cli.userspace.list', 115 'qmk.cli.userspace.path', 116 'qmk.cli.userspace.remove', 117 'qmk.cli.via2json', 118] 119 120 121def _install_deps(requirements): 122 """Perform the installation of missing requirements. 123 124 If we detect that we are running in a virtualenv we can't write into we'll use sudo to perform the pip install. 125 """ 126 command = [sys.executable, '-m', 'pip', 'install'] 127 128 if sys.prefix != sys.base_prefix: 129 # We are in a virtualenv, check to see if we need to use sudo to write to it 130 if not os.access(sys.prefix, os.W_OK): 131 print('Notice: Using sudo to install modules to location owned by root:', sys.prefix) 132 command.insert(0, 'sudo') 133 134 elif not os.access(sys.prefix, os.W_OK): 135 # We can't write to sys.prefix, attempt to install locally 136 command.append('--user') 137 138 return _run_cmd(*command, '-r', requirements) 139 140 141def _run_cmd(*command): 142 """Run a command in a subshell. 143 """ 144 if 'windows' in cli.platform.lower(): 145 safecmd = map(shlex.quote, command) 146 safecmd = ' '.join(safecmd) 147 command = [os.environ['SHELL'], '-c', safecmd] 148 149 return run(command) 150 151 152def _find_broken_requirements(requirements): 153 """ Check if the modules in the given requirements.txt are available. 154 155 Args: 156 157 requirements 158 The path to a requirements.txt file 159 160 Returns a list of modules that couldn't be imported 161 """ 162 with Path(requirements).open() as fd: 163 broken_modules = [] 164 165 for line in fd.readlines(): 166 line = line.strip().replace('<', '=').replace('>', '=') 167 168 if len(line) == 0 or line[0] == '#' or line.startswith('-r'): 169 continue 170 171 if '#' in line: 172 line = line.split('#')[0] 173 174 module_name = line.split('=')[0] if '=' in line else line 175 module_import = module_name.replace('-', '_') 176 177 # Not every module is importable by its own name. 178 if module_name in import_names: 179 module_import = import_names[module_name] 180 181 if not find_spec(module_import): 182 broken_modules.append(module_name) 183 184 return broken_modules 185 186 187def _broken_module_imports(requirements): 188 """Make sure we can import all the python modules. 189 """ 190 broken_modules = _find_broken_requirements(requirements) 191 192 for module in broken_modules: 193 print('Could not find module %s!' % module) 194 195 if broken_modules: 196 return True 197 198 return False 199 200 201def _yesno(*args): 202 """Wrapper to only prompt if interactive 203 """ 204 return sys.stdout.isatty() and yesno(*args) 205 206 207def _eprint(errmsg): 208 """Wrapper to print to stderr 209 """ 210 print(errmsg, file=sys.stderr) 211 212 213# Make sure our python is new enough 214# 215# Supported version information 216# 217# Based on the OSes we support these are the minimum python version available by default. 218# Last update: 2024 Jun 24 219# 220# Arch: 3.12 221# Debian 11: 3.9 222# Debian 12: 3.11 223# Fedora 39: 3.12 224# Fedora 40: 3.12 225# FreeBSD: 3.11 226# Gentoo: 3.12 227# macOS: 3.12 (from homebrew) 228# msys2: 3.11 229# Slackware: 3.9 230# solus: 3.10 231# Ubuntu 22.04: 3.10 232# Ubuntu 24.04: 3.12 233# void: 3.12 234 235if sys.version_info[0] != 3 or sys.version_info[1] < 9: 236 _eprint('Error: Your Python is too old! Please upgrade to Python 3.9 or later.') 237 exit(127) 238 239milc_version = __VERSION__.split('.') 240 241if int(milc_version[0]) < 2 and int(milc_version[1]) < 9: 242 requirements = Path('requirements.txt').resolve() 243 244 _eprint(f'Your MILC library is too old! Please upgrade: python3 -m pip install -U -r {str(requirements)}') 245 exit(127) 246 247# Make sure we can run binaries in the same directory as our Python interpreter 248python_dir = os.path.dirname(sys.executable) 249 250if python_dir not in os.environ['PATH'].split(os.pathsep): 251 os.environ['PATH'] = os.pathsep.join((python_dir, os.environ['PATH'])) 252 253# Check to make sure we have all our dependencies 254msg_install = f'\nPlease run `{sys.executable} -m pip install -r %s` to install required python dependencies.' 255args = sys.argv[1:] 256while args and args[0][0] == '-': 257 del args[0] 258 259safe_command = args and args[0] in safe_commands 260 261if not safe_command: 262 if _broken_module_imports('requirements.txt'): 263 if _yesno('Would you like to install the required Python modules?'): 264 _install_deps('requirements.txt') 265 else: 266 _eprint(msg_install % (str(Path('requirements.txt').resolve()),)) 267 exit(1) 268 269 if cli.config.user.developer and _broken_module_imports('requirements-dev.txt'): 270 if _yesno('Would you like to install the required developer Python modules?'): 271 _install_deps('requirements-dev.txt') 272 elif _yesno('Would you like to disable developer mode?'): 273 _run_cmd(sys.argv[0], 'config', 'user.developer=None') 274 else: 275 _eprint(msg_install % (str(Path('requirements-dev.txt').resolve()),)) 276 _eprint('You can also turn off developer mode: qmk config user.developer=None') 277 exit(1) 278 279# Import our subcommands 280for subcommand in subcommands: 281 try: 282 __import__(subcommand) 283 284 except (ImportError, ModuleNotFoundError) as e: 285 if safe_command: 286 _eprint(f'Warning: Could not import {subcommand}: {e.__class__.__name__}, {e}') 287 else: 288 raise