__init__.py (8521B)
1 """QMK CLI Subcommands 2 3 We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup. 4 """ 5 import os 6 import platform 7 import platformdirs 8 import shlex 9 import sys 10 from importlib.util import find_spec 11 from pathlib import Path 12 from subprocess import run 13 14 from milc import cli, __VERSION__ 15 from milc.questions import yesno 16 17 18 def _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. 31 QMK_DISTRIB_DIR = Path(os.environ.get('QMK_DISTRIB_DIR', _get_default_distrib_path())) 32 if 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 36 if 'QMK_PATH_PREFIX' in os.environ: 37 os.environ['PATH'] = os.environ['QMK_PATH_PREFIX'] + os.pathsep + os.environ['PATH'] 38 39 import_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 48 safe_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 57 subcommands = [ 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 121 def _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 141 def _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 152 def _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 187 def _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 201 def _yesno(*args): 202 """Wrapper to only prompt if interactive 203 """ 204 return sys.stdout.isatty() and yesno(*args) 205 206 207 def _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 235 if 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 239 milc_version = __VERSION__.split('.') 240 241 if 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 248 python_dir = os.path.dirname(sys.executable) 249 250 if 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 254 msg_install = f'\nPlease run `{sys.executable} -m pip install -r %s` to install required python dependencies.' 255 args = sys.argv[1:] 256 while args and args[0][0] == '-': 257 del args[0] 258 259 safe_command = args and args[0] in safe_commands 260 261 if 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 280 for 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