__init__.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """QMK CLI Subcommands
  2. We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
  3. """
  4. import os
  5. import shlex
  6. import sys
  7. from importlib.util import find_spec
  8. from pathlib import Path
  9. from subprocess import run
  10. from milc import cli, __VERSION__
  11. from milc.questions import yesno
  12. import_names = {
  13. # A mapping of package name to importable name
  14. 'pep8-naming': 'pep8ext_naming',
  15. 'pyusb': 'usb.core',
  16. 'qmk-dotty-dict': 'dotty_dict'
  17. }
  18. safe_commands = [
  19. # A list of subcommands we always run, even when the module imports fail
  20. 'clone',
  21. 'config',
  22. 'doctor',
  23. 'env',
  24. 'setup',
  25. ]
  26. subcommands = [
  27. 'qmk.cli.bux',
  28. 'qmk.cli.c2json',
  29. 'qmk.cli.cformat',
  30. 'qmk.cli.chibios.confmigrate',
  31. 'qmk.cli.clean',
  32. 'qmk.cli.compile',
  33. 'qmk.cli.console',
  34. 'qmk.cli.docs',
  35. 'qmk.cli.doctor',
  36. 'qmk.cli.fileformat',
  37. 'qmk.cli.flash',
  38. 'qmk.cli.format.c',
  39. 'qmk.cli.format.json',
  40. 'qmk.cli.format.python',
  41. 'qmk.cli.format.text',
  42. 'qmk.cli.generate.api',
  43. 'qmk.cli.generate.config_h',
  44. 'qmk.cli.generate.dfu_header',
  45. 'qmk.cli.generate.docs',
  46. 'qmk.cli.generate.info_json',
  47. 'qmk.cli.generate.keyboard_h',
  48. 'qmk.cli.generate.layouts',
  49. 'qmk.cli.generate.rgb_breathe_table',
  50. 'qmk.cli.generate.rules_mk',
  51. 'qmk.cli.generate.version_h',
  52. 'qmk.cli.hello',
  53. 'qmk.cli.info',
  54. 'qmk.cli.json2c',
  55. 'qmk.cli.lint',
  56. 'qmk.cli.list.keyboards',
  57. 'qmk.cli.list.keymaps',
  58. 'qmk.cli.kle2json',
  59. 'qmk.cli.multibuild',
  60. 'qmk.cli.new.keyboard',
  61. 'qmk.cli.new.keymap',
  62. 'qmk.cli.pyformat',
  63. 'qmk.cli.pytest',
  64. ]
  65. def _run_cmd(*command):
  66. """Run a command in a subshell.
  67. """
  68. if 'windows' in cli.platform.lower():
  69. safecmd = map(shlex.quote, command)
  70. safecmd = ' '.join(safecmd)
  71. command = [os.environ['SHELL'], '-c', safecmd]
  72. return run(command)
  73. def _find_broken_requirements(requirements):
  74. """ Check if the modules in the given requirements.txt are available.
  75. Args:
  76. requirements
  77. The path to a requirements.txt file
  78. Returns a list of modules that couldn't be imported
  79. """
  80. with Path(requirements).open() as fd:
  81. broken_modules = []
  82. for line in fd.readlines():
  83. line = line.strip().replace('<', '=').replace('>', '=')
  84. if len(line) == 0 or line[0] == '#' or line.startswith('-r'):
  85. continue
  86. if '#' in line:
  87. line = line.split('#')[0]
  88. module_name = line.split('=')[0] if '=' in line else line
  89. module_import = module_name.replace('-', '_')
  90. # Not every module is importable by its own name.
  91. if module_name in import_names:
  92. module_import = import_names[module_name]
  93. if not find_spec(module_import):
  94. broken_modules.append(module_name)
  95. return broken_modules
  96. def _broken_module_imports(requirements):
  97. """Make sure we can import all the python modules.
  98. """
  99. broken_modules = _find_broken_requirements(requirements)
  100. for module in broken_modules:
  101. print('Could not find module %s!' % module)
  102. if broken_modules:
  103. return True
  104. return False
  105. # Make sure our python is new enough
  106. #
  107. # Supported version information
  108. #
  109. # Based on the OSes we support these are the minimum python version available by default.
  110. # Last update: 2021 Jan 02
  111. #
  112. # Arch: 3.9
  113. # Debian: 3.7
  114. # Fedora 31: 3.7
  115. # Fedora 32: 3.8
  116. # Fedora 33: 3.9
  117. # FreeBSD: 3.7
  118. # Gentoo: 3.7
  119. # macOS: 3.9 (from homebrew)
  120. # msys2: 3.8
  121. # Slackware: 3.7
  122. # solus: 3.7
  123. # void: 3.9
  124. if sys.version_info[0] != 3 or sys.version_info[1] < 7:
  125. print('Error: Your Python is too old! Please upgrade to Python 3.7 or later.')
  126. exit(127)
  127. milc_version = __VERSION__.split('.')
  128. if int(milc_version[0]) < 2 and int(milc_version[1]) < 4:
  129. requirements = Path('requirements.txt').resolve()
  130. print(f'Your MILC library is too old! Please upgrade: python3 -m pip install -U -r {str(requirements)}')
  131. exit(127)
  132. # Check to make sure we have all our dependencies
  133. msg_install = 'Please run `python3 -m pip install -r %s` to install required python dependencies.'
  134. args = sys.argv[1:]
  135. while args and args[0][0] == '-':
  136. del args[0]
  137. safe_command = args and args[0] in safe_commands
  138. if not safe_command:
  139. if _broken_module_imports('requirements.txt'):
  140. if yesno('Would you like to install the required Python modules?'):
  141. _run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt')
  142. else:
  143. print()
  144. print(msg_install % (str(Path('requirements.txt').resolve()),))
  145. print()
  146. exit(1)
  147. if cli.config.user.developer and _broken_module_imports('requirements-dev.txt'):
  148. if yesno('Would you like to install the required developer Python modules?'):
  149. _run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements-dev.txt')
  150. elif yesno('Would you like to disable developer mode?'):
  151. _run_cmd(sys.argv[0], 'config', 'user.developer=None')
  152. else:
  153. print()
  154. print(msg_install % (str(Path('requirements-dev.txt').resolve()),))
  155. print('You can also turn off developer mode: qmk config user.developer=None')
  156. print()
  157. exit(1)
  158. # Import our subcommands
  159. for subcommand in subcommands:
  160. try:
  161. __import__(subcommand)
  162. except (ImportError, ModuleNotFoundError) as e:
  163. if safe_command:
  164. print(f'Warning: Could not import {subcommand}: {e.__class__.__name__}, {e}')
  165. else:
  166. raise