qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

__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