diff options
Diffstat (limited to 'lib/python/qmk')
33 files changed, 1156 insertions, 410 deletions
| diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index de71a5d1e7..b22f1c0d2d 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -40,7 +40,10 @@ subcommands = [      'qmk.cli.doctor',      'qmk.cli.fileformat',      'qmk.cli.flash', +    'qmk.cli.format.c',      'qmk.cli.format.json', +    'qmk.cli.format.python', +    'qmk.cli.format.text',      'qmk.cli.generate.api',      'qmk.cli.generate.config_h',      'qmk.cli.generate.dfu_header', @@ -50,6 +53,7 @@ subcommands = [      'qmk.cli.generate.layouts',      'qmk.cli.generate.rgb_breathe_table',      'qmk.cli.generate.rules_mk', +    'qmk.cli.generate.version_h',      'qmk.cli.hello',      'qmk.cli.info',      'qmk.cli.json2c', diff --git a/lib/python/qmk/cli/cformat.py b/lib/python/qmk/cli/cformat.py index efeb459676..9d0ecaeba3 100644..100755 --- a/lib/python/qmk/cli/cformat.py +++ b/lib/python/qmk/cli/cformat.py @@ -1,137 +1,28 @@ -"""Format C code according to QMK's style. +"""Point people to the new command name.  """ -from os import path -from shutil import which -from subprocess import CalledProcessError, DEVNULL, Popen, PIPE +import sys +from pathlib import Path -from argcomplete.completers import FilesCompleter  from milc import cli -from qmk.path import normpath -from qmk.c_parse import c_source_files - -c_file_suffixes = ('c', 'h', 'cpp') -core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') -ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios') - - -def find_clang_format(): -    """Returns the path to clang-format. -    """ -    for clang_version in range(20, 6, -1): -        binary = f'clang-format-{clang_version}' - -        if which(binary): -            return binary - -    return 'clang-format' - - -def find_diffs(files): -    """Run clang-format and diff it against a file. -    """ -    found_diffs = False - -    for file in files: -        cli.log.debug('Checking for changes in %s', file) -        clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True) -        diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True) - -        if diff.returncode != 0: -            print(diff.stdout) -            found_diffs = True - -    return found_diffs - - -def cformat_run(files): -    """Spawn clang-format subprocess with proper arguments -    """ -    # Determine which version of clang-format to use -    clang_format = [find_clang_format(), '-i'] - -    try: -        cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL) -        cli.log.info('Successfully formatted the C code.') -        return True - -    except CalledProcessError as e: -        cli.log.error('Error formatting C code!') -        cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode) -        cli.log.debug('STDOUT:') -        cli.log.debug(e.stdout) -        cli.log.debug('STDERR:') -        cli.log.debug(e.stderr) -        return False - - -def filter_files(files, core_only=False): -    """Yield only files to be formatted and skip the rest -    """ -    if core_only: -        # Filter non-core files -        for index, file in enumerate(files): -            # The following statement checks each file to see if the file path is -            # - in the core directories -            # - not in the ignored directories -            if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored): -                files[index] = None -                cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file) - -    for file in files: -        if file and file.name.split('.')[-1] in c_file_suffixes: -            yield file -        else: -            cli.log.debug('Skipping file %s', file) -  @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.")  @cli.argument('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.')  @cli.argument('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.')  @cli.argument('--core-only', arg_only=True, action='store_true', help='Format core files only.') [email protected]('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.') [email protected]("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True) [email protected]('files', nargs='*', arg_only=True, help='Filename(s) to format.') [email protected]('Pointer to the new command name: qmk format-c.', hidden=True)  def cformat(cli): -    """Format C code according to QMK's style. +    """Pointer to the new command name: qmk format-c.      """ -    # Find the list of files to format -    if cli.args.files: -        files = list(filter_files(cli.args.files, cli.args.core_only)) - -        if not files: -            cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files))) -            exit(0) - -        if cli.args.all_files: -            cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files))) - -    elif cli.args.all_files: -        all_files = c_source_files(core_dirs) -        files = list(filter_files(all_files, True)) - -    else: -        git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs] -        git_diff = cli.run(git_diff_cmd, stdin=DEVNULL) - -        if git_diff.returncode != 0: -            cli.log.error("Error running %s", git_diff_cmd) -            print(git_diff.stderr) -            return git_diff.returncode - -        files = [] - -        for file in git_diff.stdout.strip().split('\n'): -            if not any([file.startswith(ignore) for ignore in ignored]): -                if path.exists(file) and file.split('.')[-1] in c_file_suffixes: -                    files.append(file) +    cli.log.warning('"qmk cformat" has been renamed to "qmk format-c". Please use the new command in the future.') +    argv = [sys.executable, *sys.argv] +    argv[argv.index('cformat')] = 'format-c' +    script_path = Path(argv[1]) +    script_path_exe = Path(f'{argv[1]}.exe') -    # Sanity check -    if not files: -        cli.log.error('No changed files detected. Use "qmk cformat -a" to format all core files') -        return False +    if not script_path.exists() and script_path_exe.exists(): +        # For reasons I don't understand ".exe" is stripped from the script name on windows. +        argv[1] = str(script_path_exe) -    # Run clang-format on the files we've found -    if cli.args.dry_run: -        return not find_diffs(files) -    else: -        return cformat_run(files) +    return cli.run(argv, capture_output=False).returncode diff --git a/lib/python/qmk/cli/compile.py b/lib/python/qmk/cli/compile.py index 7a45e77214..acbd778649 100755 --- a/lib/python/qmk/cli/compile.py +++ b/lib/python/qmk/cli/compile.py @@ -18,7 +18,7 @@ from qmk.keymap import keymap_completer  @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')  @cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')  @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")  @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")  @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")  @cli.subcommand('Compile a QMK Firmware.') diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index 45ff0c8bee..3c508160e3 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -48,10 +48,11 @@ KNOWN_BOOTLOADERS = {      ('239A', '000C'): 'caterina: Adafruit Feather 32U4',      ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v',      ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v', -    ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',      ('2A03', '0036'): 'caterina: Arduino Leonardo',      ('2A03', '0037'): 'caterina: Arduino Micro', -    ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode' +    ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode', +    ('03EB', '2067'): 'qmk-hid: HID Bootloader', +    ('03EB', '2045'): 'lufa-ms: LUFA Mass Storage Bootloader'  } diff --git a/lib/python/qmk/cli/doctor/__init__.py b/lib/python/qmk/cli/doctor/__init__.py new file mode 100755 index 0000000000..272e042023 --- /dev/null +++ b/lib/python/qmk/cli/doctor/__init__.py @@ -0,0 +1,5 @@ +"""QMK Doctor + +Check out the user's QMK environment and make sure it's ready to compile. +""" +from .main import doctor diff --git a/lib/python/qmk/os_helpers/__init__.py b/lib/python/qmk/cli/doctor/check.py index 3e98db3c32..0807f41518 100644 --- a/lib/python/qmk/os_helpers/__init__.py +++ b/lib/python/qmk/cli/doctor/check.py @@ -1,4 +1,4 @@ -"""OS-agnostic helper functions +"""Check for specific programs.  """  from enum import Enum  import re @@ -30,7 +30,7 @@ ESSENTIAL_BINARIES = {  } -def parse_gcc_version(version): +def _parse_gcc_version(version):      m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)      return { @@ -40,7 +40,7 @@ def parse_gcc_version(version):      } -def check_arm_gcc_version(): +def _check_arm_gcc_version():      """Returns True if the arm-none-eabi-gcc version is not known to cause problems.      """      if 'output' in ESSENTIAL_BINARIES['arm-none-eabi-gcc']: @@ -50,7 +50,7 @@ def check_arm_gcc_version():      return CheckStatus.OK  # Right now all known arm versions are ok -def check_avr_gcc_version(): +def _check_avr_gcc_version():      """Returns True if the avr-gcc version is not known to cause problems.      """      rc = CheckStatus.ERROR @@ -60,7 +60,7 @@ def check_avr_gcc_version():          cli.log.info('Found avr-gcc version %s', version_number)          rc = CheckStatus.OK -        parsed_version = parse_gcc_version(version_number) +        parsed_version = _parse_gcc_version(version_number)          if parsed_version['major'] > 8:              cli.log.warning('{fg_yellow}We do not recommend avr-gcc newer than 8. Downgrading to 8.x is recommended.')              rc = CheckStatus.WARNING @@ -68,7 +68,7 @@ def check_avr_gcc_version():      return rc -def check_avrdude_version(): +def _check_avrdude_version():      if 'output' in ESSENTIAL_BINARIES['avrdude']:          last_line = ESSENTIAL_BINARIES['avrdude']['output'].split('\n')[-2]          version_number = last_line.split()[2][:-1] @@ -77,7 +77,7 @@ def check_avrdude_version():      return CheckStatus.OK -def check_dfu_util_version(): +def _check_dfu_util_version():      if 'output' in ESSENTIAL_BINARIES['dfu-util']:          first_line = ESSENTIAL_BINARIES['dfu-util']['output'].split('\n')[0]          version_number = first_line.split()[1] @@ -86,7 +86,7 @@ def check_dfu_util_version():      return CheckStatus.OK -def check_dfu_programmer_version(): +def _check_dfu_programmer_version():      if 'output' in ESSENTIAL_BINARIES['dfu-programmer']:          first_line = ESSENTIAL_BINARIES['dfu-programmer']['output'].split('\n')[0]          version_number = first_line.split()[1] @@ -111,7 +111,7 @@ def check_binary_versions():      """Check the versions of ESSENTIAL_BINARIES      """      versions = [] -    for check in (check_arm_gcc_version, check_avr_gcc_version, check_avrdude_version, check_dfu_util_version, check_dfu_programmer_version): +    for check in (_check_arm_gcc_version, _check_avr_gcc_version, _check_avrdude_version, _check_dfu_util_version, _check_dfu_programmer_version):          versions.append(check())      return versions @@ -159,6 +159,6 @@ def check_git_repo():      This is a decent enough indicator that the qmk_firmware directory is a      proper Git repository, rather than a .zip download from GitHub.      """ -    dot_git_dir = QMK_FIRMWARE / '.git' +    dot_git = QMK_FIRMWARE / '.git' -    return CheckStatus.OK if dot_git_dir.is_dir() else CheckStatus.WARNING +    return CheckStatus.OK if dot_git.exists() else CheckStatus.WARNING diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/cli/doctor/linux.py index 008654ab0f..6ce00f6ef1 100644 --- a/lib/python/qmk/os_helpers/linux/__init__.py +++ b/lib/python/qmk/cli/doctor/linux.py @@ -1,11 +1,13 @@  """OS-specific functions for: Linux  """ -from pathlib import Path +import platform  import shutil +from pathlib import Path  from milc import cli +  from qmk.constants import QMK_FIRMWARE -from qmk.os_helpers import CheckStatus +from .check import CheckStatus  def _udev_rule(vid, pid=None, *args): @@ -39,7 +41,12 @@ def check_udev_rules():      """Make sure the udev rules look good.      """      rc = CheckStatus.OK -    udev_dir = Path("/etc/udev/rules.d/") +    udev_dirs = [ +        Path("/usr/lib/udev/rules.d/"), +        Path("/usr/local/lib/udev/rules.d/"), +        Path("/run/udev/rules.d/"), +        Path("/etc/udev/rules.d/"), +    ]      desired_rules = {          'atmel-dfu': {              _udev_rule("03eb", "2fef"),  # ATmega16U2 @@ -75,6 +82,10 @@ def check_udev_rules():              # dog hunter AG              _udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'),  # Leonardo              _udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"')  # Micro +        }, +        'hid-bootloader': { +            _udev_rule("03eb", "2067"),  # QMK HID +            _udev_rule("16c0", "0478")  # PJRC halfkay          }      } @@ -88,8 +99,8 @@ def check_udev_rules():          'tmk': {_deprecated_udev_rule("feed")}      } -    if udev_dir.exists(): -        udev_rules = [rule_file for rule_file in udev_dir.glob('*.rules')] +    if any(udev_dir.exists() for udev_dir in udev_dirs): +        udev_rules = [rule_file for udev_dir in udev_dirs for rule_file in udev_dir.glob('*.rules')]          current_rules = set()          # Collect all rules from the config files @@ -115,7 +126,8 @@ def check_udev_rules():                      cli.log.warning("{fg_yellow}Missing or outdated udev rules for '%s' boards. Run 'sudo cp %s/util/udev/50-qmk.rules /etc/udev/rules.d/'.", bootloader, QMK_FIRMWARE)      else: -        cli.log.warning("{fg_yellow}'%s' does not exist. Skipping udev rule checking...", udev_dir) +        cli.log.warning("{fg_yellow}Can't find udev rules, skipping udev rule checking...") +        cli.log.debug("Checked directories: %s", ', '.join(str(udev_dir) for udev_dir in udev_dirs))      return rc @@ -138,3 +150,23 @@ def check_modem_manager():          """(TODO): Add check for non-systemd systems          """      return False + + +def os_test_linux(): +    """Run the Linux specific tests. +    """ +    # Don't bother with udev on WSL, for now +    if 'microsoft' in platform.uname().release.lower(): +        cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.") + +        # https://github.com/microsoft/WSL/issues/4197 +        if QMK_FIRMWARE.as_posix().startswith("/mnt"): +            cli.log.warning("I/O performance on /mnt may be extremely slow.") +            return CheckStatus.WARNING + +        return CheckStatus.OK +    else: +        cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") +        from .linux import check_udev_rules + +        return check_udev_rules() diff --git a/lib/python/qmk/cli/doctor/macos.py b/lib/python/qmk/cli/doctor/macos.py new file mode 100644 index 0000000000..00fb272858 --- /dev/null +++ b/lib/python/qmk/cli/doctor/macos.py @@ -0,0 +1,13 @@ +import platform + +from milc import cli + +from .check import CheckStatus + + +def os_test_macos(): +    """Run the Mac specific tests. +    """ +    cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) + +    return CheckStatus.OK diff --git a/lib/python/qmk/cli/doctor.py b/lib/python/qmk/cli/doctor/main.py index 327bc9cb30..6a31ccdfdd 100755 --- a/lib/python/qmk/cli/doctor.py +++ b/lib/python/qmk/cli/doctor/main.py @@ -7,9 +7,11 @@ from subprocess import DEVNULL  from milc import cli  from milc.questions import yesno +  from qmk import submodules -from qmk.constants import QMK_FIRMWARE -from qmk.os_helpers import CheckStatus, check_binaries, check_binary_versions, check_submodules, check_git_repo +from qmk.constants import QMK_FIRMWARE, QMK_FIRMWARE_UPSTREAM +from .check import CheckStatus, check_binaries, check_binary_versions, check_submodules +from qmk.commands import git_check_repo, git_get_branch, git_is_dirty, git_get_remotes, git_check_deviation, in_virtualenv  def os_tests(): @@ -18,51 +20,48 @@ def os_tests():      platform_id = platform.platform().lower()      if 'darwin' in platform_id or 'macos' in platform_id: +        from .macos import os_test_macos          return os_test_macos()      elif 'linux' in platform_id: +        from .linux import os_test_linux          return os_test_linux()      elif 'windows' in platform_id: +        from .windows import os_test_windows          return os_test_windows()      else:          cli.log.warning('Unsupported OS detected: %s', platform_id)          return CheckStatus.WARNING -def os_test_linux(): -    """Run the Linux specific tests. +def git_tests(): +    """Run Git-related checks      """ -    # Don't bother with udev on WSL, for now -    if 'microsoft' in platform.uname().release.lower(): -        cli.log.info("Detected {fg_cyan}Linux (WSL){fg_reset}.") - -        # https://github.com/microsoft/WSL/issues/4197 -        if QMK_FIRMWARE.as_posix().startswith("/mnt"): -            cli.log.warning("I/O performance on /mnt may be extremely slow.") -            return CheckStatus.WARNING +    status = CheckStatus.OK -        return CheckStatus.OK +    # Make sure our QMK home is a Git repo +    git_ok = git_check_repo() +    if not git_ok: +        cli.log.warning("{fg_yellow}QMK home does not appear to be a Git repository! (no .git folder)") +        status = CheckStatus.WARNING      else: -        cli.log.info("Detected {fg_cyan}Linux{fg_reset}.") -        from qmk.os_helpers.linux import check_udev_rules - -        return check_udev_rules() - - -def os_test_macos(): -    """Run the Mac specific tests. -    """ -    cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) - -    return CheckStatus.OK - - -def os_test_windows(): -    """Run the Windows specific tests. -    """ -    win32_ver = platform.win32_ver() -    cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1]) - -    return CheckStatus.OK +        git_branch = git_get_branch() +        if git_branch: +            cli.log.info('Git branch: %s', git_branch) +            git_dirty = git_is_dirty() +            if git_dirty: +                cli.log.warning('{fg_yellow}Git has unstashed/uncommitted changes.') +                status = CheckStatus.WARNING +            git_remotes = git_get_remotes() +            if 'upstream' not in git_remotes.keys() or QMK_FIRMWARE_UPSTREAM not in git_remotes['upstream'].get('url', ''): +                cli.log.warning('{fg_yellow}The official repository does not seem to be configured as git remote "upstream".') +                status = CheckStatus.WARNING +            else: +                git_deviation = git_check_deviation(git_branch) +                if git_branch in ['master', 'develop'] and git_deviation: +                    cli.log.warning('{fg_yellow}The local "%s" branch contains commits not found in the upstream branch.', git_branch) +                    status = CheckStatus.WARNING + +    return status  @cli.argument('-y', '--yes', action='store_true', arg_only=True, help='Answer yes to all questions.') @@ -82,12 +81,11 @@ def doctor(cli):      status = os_tests() -    # Make sure our QMK home is a Git repo -    git_ok = check_git_repo() +    status = git_tests() -    if git_ok == CheckStatus.WARNING: -        cli.log.warning("QMK home does not appear to be a Git repository! (no .git folder)") -        status = CheckStatus.WARNING +    venv = in_virtualenv() +    if venv: +        cli.log.info('CLI installed in virtualenv.')      # Make sure the basic CLI tools we need are available and can be executed.      bin_ok = check_binaries() diff --git a/lib/python/qmk/cli/doctor/windows.py b/lib/python/qmk/cli/doctor/windows.py new file mode 100644 index 0000000000..381ab36fde --- /dev/null +++ b/lib/python/qmk/cli/doctor/windows.py @@ -0,0 +1,14 @@ +import platform + +from milc import cli + +from .check import CheckStatus + + +def os_test_windows(): +    """Run the Windows specific tests. +    """ +    win32_ver = platform.win32_ver() +    cli.log.info("Detected {fg_cyan}Windows %s (%s){fg_reset}.", win32_ver[0], win32_ver[1]) + +    return CheckStatus.OK diff --git a/lib/python/qmk/cli/fileformat.py b/lib/python/qmk/cli/fileformat.py index 112d8d59da..cee4ba1acd 100644..100755 --- a/lib/python/qmk/cli/fileformat.py +++ b/lib/python/qmk/cli/fileformat.py @@ -1,13 +1,23 @@ -"""Format files according to QMK's style. +"""Point people to the new command name.  """ -from milc import cli +import sys +from pathlib import Path -import subprocess +from milc import cli [email protected]("Format files according to QMK's style.", hidden=True) [email protected]('Pointer to the new command name: qmk format-text.', hidden=True)  def fileformat(cli): -    """Run several general formatting commands. +    """Pointer to the new command name: qmk format-text.      """ -    dos2unix = subprocess.run(['bash', '-c', 'git ls-files -z | xargs -0 dos2unix'], stdout=subprocess.DEVNULL) -    return dos2unix.returncode +    cli.log.warning('"qmk fileformat" has been renamed to "qmk format-text". Please use the new command in the future.') +    argv = [sys.executable, *sys.argv] +    argv[argv.index('fileformat')] = 'format-text' +    script_path = Path(argv[1]) +    script_path_exe = Path(f'{argv[1]}.exe') + +    if not script_path.exists() and script_path_exe.exists(): +        # For reasons I don't understand ".exe" is stripped from the script name on windows. +        argv[1] = str(script_path_exe) + +    return cli.run(argv, capture_output=False).returncode diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py index 1b2932a5b2..c2d9e09c69 100644 --- a/lib/python/qmk/cli/flash.py +++ b/lib/python/qmk/cli/flash.py @@ -38,7 +38,7 @@ def print_bootloader_help():  @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')  @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')  @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")  @cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")  @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")  @cli.subcommand('QMK Flash.') diff --git a/lib/python/qmk/cli/format/c.py b/lib/python/qmk/cli/format/c.py new file mode 100644 index 0000000000..b7263e19f3 --- /dev/null +++ b/lib/python/qmk/cli/format/c.py @@ -0,0 +1,137 @@ +"""Format C code according to QMK's style. +""" +from os import path +from shutil import which +from subprocess import CalledProcessError, DEVNULL, Popen, PIPE + +from argcomplete.completers import FilesCompleter +from milc import cli + +from qmk.path import normpath +from qmk.c_parse import c_source_files + +c_file_suffixes = ('c', 'h', 'cpp') +core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms') +ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios') + + +def find_clang_format(): +    """Returns the path to clang-format. +    """ +    for clang_version in range(20, 6, -1): +        binary = f'clang-format-{clang_version}' + +        if which(binary): +            return binary + +    return 'clang-format' + + +def find_diffs(files): +    """Run clang-format and diff it against a file. +    """ +    found_diffs = False + +    for file in files: +        cli.log.debug('Checking for changes in %s', file) +        clang_format = Popen([find_clang_format(), file], stdout=PIPE, stderr=PIPE, universal_newlines=True) +        diff = cli.run(['diff', '-u', f'--label=a/{file}', f'--label=b/{file}', str(file), '-'], stdin=clang_format.stdout, capture_output=True) + +        if diff.returncode != 0: +            print(diff.stdout) +            found_diffs = True + +    return found_diffs + + +def cformat_run(files): +    """Spawn clang-format subprocess with proper arguments +    """ +    # Determine which version of clang-format to use +    clang_format = [find_clang_format(), '-i'] + +    try: +        cli.run([*clang_format, *map(str, files)], check=True, capture_output=False, stdin=DEVNULL) +        cli.log.info('Successfully formatted the C code.') +        return True + +    except CalledProcessError as e: +        cli.log.error('Error formatting C code!') +        cli.log.debug('%s exited with returncode %s', e.cmd, e.returncode) +        cli.log.debug('STDOUT:') +        cli.log.debug(e.stdout) +        cli.log.debug('STDERR:') +        cli.log.debug(e.stderr) +        return False + + +def filter_files(files, core_only=False): +    """Yield only files to be formatted and skip the rest +    """ +    if core_only: +        # Filter non-core files +        for index, file in enumerate(files): +            # The following statement checks each file to see if the file path is +            # - in the core directories +            # - not in the ignored directories +            if not any(i in str(file) for i in core_dirs) or any(i in str(file) for i in ignored): +                files[index] = None +                cli.log.debug("Skipping non-core file %s, as '--core-only' is used.", file) + +    for file in files: +        if file and file.name.split('.')[-1] in c_file_suffixes: +            yield file +        else: +            cli.log.debug('Skipping file %s', file) + + [email protected]('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") [email protected]('-b', '--base-branch', default='origin/master', help='Branch to compare to diffs to.') [email protected]('-a', '--all-files', arg_only=True, action='store_true', help='Format all core files.') [email protected]('--core-only', arg_only=True, action='store_true', help='Format core files only.') [email protected]('files', nargs='*', arg_only=True, type=normpath, completer=FilesCompleter('.c'), help='Filename(s) to format.') [email protected]("Format C code according to QMK's style.", hidden=False if cli.config.user.developer else True) +def format_c(cli): +    """Format C code according to QMK's style. +    """ +    # Find the list of files to format +    if cli.args.files: +        files = list(filter_files(cli.args.files, cli.args.core_only)) + +        if not files: +            cli.log.error('No C files in filelist: %s', ', '.join(map(str, cli.args.files))) +            exit(0) + +        if cli.args.all_files: +            cli.log.warning('Filenames passed with -a, only formatting: %s', ','.join(map(str, files))) + +    elif cli.args.all_files: +        all_files = c_source_files(core_dirs) +        files = list(filter_files(all_files, True)) + +    else: +        git_diff_cmd = ['git', 'diff', '--name-only', cli.args.base_branch, *core_dirs] +        git_diff = cli.run(git_diff_cmd, stdin=DEVNULL) + +        if git_diff.returncode != 0: +            cli.log.error("Error running %s", git_diff_cmd) +            print(git_diff.stderr) +            return git_diff.returncode + +        files = [] + +        for file in git_diff.stdout.strip().split('\n'): +            if not any([file.startswith(ignore) for ignore in ignored]): +                if path.exists(file) and file.split('.')[-1] in c_file_suffixes: +                    files.append(file) + +    # Sanity check +    if not files: +        cli.log.error('No changed files detected. Use "qmk format-c -a" to format all core files') +        return False + +    # Run clang-format on the files we've found +    if cli.args.dry_run: +        return not find_diffs(files) +    else: +        return cformat_run(files) diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 1358c70e7a..19d504491f 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py @@ -8,7 +8,7 @@ from jsonschema import ValidationError  from milc import cli  from qmk.info import info_json -from qmk.json_schema import json_load, keyboard_validate +from qmk.json_schema import json_load, validate  from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder  from qmk.path import normpath @@ -23,14 +23,13 @@ def format_json(cli):      if cli.args.format == 'auto':          try: -            keyboard_validate(json_file) +            validate(json_file, 'qmk.keyboard.v1')              json_encoder = InfoJSONEncoder          except ValidationError as e:              cli.log.warning('File %s did not validate as a keyboard:\n\t%s', cli.args.json_file, e)              cli.log.info('Treating %s as a keymap file.', cli.args.json_file)              json_encoder = KeymapJSONEncoder -      elif cli.args.format == 'keyboard':          json_encoder = InfoJSONEncoder      elif cli.args.format == 'keymap': diff --git a/lib/python/qmk/cli/format/python.py b/lib/python/qmk/cli/format/python.py new file mode 100755 index 0000000000..00612f97ec --- /dev/null +++ b/lib/python/qmk/cli/format/python.py @@ -0,0 +1,26 @@ +"""Format python code according to QMK's style. +""" +from subprocess import CalledProcessError, DEVNULL + +from milc import cli + + [email protected]('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.") [email protected]("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True) +def format_python(cli): +    """Format python code according to QMK's style. +    """ +    edit = '--diff' if cli.args.dry_run else '--in-place' +    yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python'] +    try: +        cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) +        cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.') +        return True + +    except CalledProcessError: +        if cli.args.dry_run: +            cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!') +        else: +            cli.log.error('Error formatting python code!') + +    return False diff --git a/lib/python/qmk/cli/format/text.py b/lib/python/qmk/cli/format/text.py new file mode 100644 index 0000000000..e7e07b7297 --- /dev/null +++ b/lib/python/qmk/cli/format/text.py @@ -0,0 +1,27 @@ +"""Ensure text files have the proper line endings. +""" +from subprocess import CalledProcessError + +from milc import cli + + [email protected]("Ensure text files have the proper line endings.", hidden=True) +def format_text(cli): +    """Ensure text files have the proper line endings. +    """ +    try: +        file_list_cmd = cli.run(['git', 'ls-files', '-z'], check=True) +    except CalledProcessError as e: +        cli.log.error('Could not get file list: %s', e) +        exit(1) +    except Exception as e: +        cli.log.error('Unhandled exception: %s: %s', e.__class__.__name__, e) +        cli.log.exception(e) +        exit(1) + +    dos2unix = cli.run(['xargs', '-0', 'dos2unix'], stdin=None, input=file_list_cmd.stdout) + +    if dos2unix.returncode != 0: +        print(dos2unix.stderr) + +    return dos2unix.returncode diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 54cd5b96a8..ca7e14fe6b 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -5,14 +5,14 @@ from pathlib import Path  from dotty_dict import dotty  from milc import cli -from qmk.decorators import automagic_keyboard, automagic_keymap  from qmk.info import info_json -from qmk.json_schema import json_load +from qmk.json_schema import json_load, validate  from qmk.keyboard import keyboard_completer, keyboard_folder -from qmk.path import is_keyboard, normpath +from qmk.keymap import locate_keymap +from qmk.path import normpath -def direct_pins(direct_pins): +def direct_pins(direct_pins, postfix):      """Return the config.h lines that set the direct pins.      """      rows = [] @@ -24,81 +24,60 @@ def direct_pins(direct_pins):      col_count = len(direct_pins[0])      row_count = len(direct_pins) -    return """ -#ifndef MATRIX_COLS -#   define MATRIX_COLS %s -#endif // MATRIX_COLS +    return f""" +#ifndef MATRIX_COLS{postfix} +#   define MATRIX_COLS{postfix} {col_count} +#endif // MATRIX_COLS{postfix} -#ifndef MATRIX_ROWS -#   define MATRIX_ROWS %s -#endif // MATRIX_ROWS +#ifndef MATRIX_ROWS{postfix} +#   define MATRIX_ROWS{postfix} {row_count} +#endif // MATRIX_ROWS{postfix} -#ifndef DIRECT_PINS -#   define DIRECT_PINS {%s} -#endif // DIRECT_PINS -""" % (col_count, row_count, ','.join(rows)) +#ifndef DIRECT_PINS{postfix} +#   define DIRECT_PINS{postfix} {{ {", ".join(rows)} }} +#endif // DIRECT_PINS{postfix} +""" -def pin_array(define, pins): +def pin_array(define, pins, postfix):      """Return the config.h lines that set a pin array.      """      pin_num = len(pins)      pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))      return f""" -#ifndef {define}S -#   define {define}S {pin_num} -#endif // {define}S +#ifndef {define}S{postfix} +#   define {define}S{postfix} {pin_num} +#endif // {define}S{postfix} -#ifndef {define}_PINS -#   define {define}_PINS {{ {pin_array} }} -#endif // {define}_PINS +#ifndef {define}_PINS{postfix} +#   define {define}_PINS{postfix} {{ {pin_array} }} +#endif // {define}_PINS{postfix}  """ -def matrix_pins(matrix_pins): +def matrix_pins(matrix_pins, postfix=''):      """Add the matrix config to the config.h.      """      pins = []      if 'direct' in matrix_pins: -        pins.append(direct_pins(matrix_pins['direct'])) +        pins.append(direct_pins(matrix_pins['direct'], postfix))      if 'cols' in matrix_pins: -        pins.append(pin_array('MATRIX_COL', matrix_pins['cols'])) +        pins.append(pin_array('MATRIX_COL', matrix_pins['cols'], postfix))      if 'rows' in matrix_pins: -        pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'])) +        pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'], postfix))      return '\n'.join(pins) [email protected]('-o', '--output', arg_only=True, type=normpath, help='File to write to') [email protected]('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.') [email protected]('Used by the make system to generate info_config.h from info.json', hidden=True) -@automagic_keyboard -@automagic_keymap -def generate_config_h(cli): -    """Generates the info_config.h file. +def generate_config_items(kb_info_json, config_h_lines): +    """Iterate through the info_config map to generate basic config values.      """ -    # Determine our keyboard(s) -    if not cli.config.generate_config_h.keyboard: -        cli.log.error('Missing parameter: --keyboard') -        cli.subcommands['info'].print_help() -        return False - -    if not is_keyboard(cli.config.generate_config_h.keyboard): -        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard) -        return False - -    # Build the info_config.h file. -    kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))      info_config_map = json_load(Path('data/mappings/info_config.json')) -    config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once'] - -    # Iterate through the info_config map to generate basic things      for config_key, info_dict in info_config_map.items():          info_key = info_dict['info_key']          key_type = info_dict.get('value_type', 'str') @@ -135,9 +114,75 @@ def generate_config_h(cli):              config_h_lines.append(f'#   define {config_key} {config_value}')              config_h_lines.append(f'#endif // {config_key}') + +def generate_split_config(kb_info_json, config_h_lines): +    """Generate the config.h lines for split boards.""" +    if 'primary' in kb_info_json['split']: +        if kb_info_json['split']['primary'] in ('left', 'right'): +            config_h_lines.append('') +            config_h_lines.append('#ifndef MASTER_LEFT') +            config_h_lines.append('#   ifndef MASTER_RIGHT') +            if kb_info_json['split']['primary'] == 'left': +                config_h_lines.append('#       define MASTER_LEFT') +            elif kb_info_json['split']['primary'] == 'right': +                config_h_lines.append('#       define MASTER_RIGHT') +            config_h_lines.append('#   endif // MASTER_RIGHT') +            config_h_lines.append('#endif // MASTER_LEFT') +        elif kb_info_json['split']['primary'] == 'pin': +            config_h_lines.append('') +            config_h_lines.append('#ifndef SPLIT_HAND_PIN') +            config_h_lines.append('#   define SPLIT_HAND_PIN') +            config_h_lines.append('#endif // SPLIT_HAND_PIN') +        elif kb_info_json['split']['primary'] == 'matrix_grid': +            config_h_lines.append('') +            config_h_lines.append('#ifndef SPLIT_HAND_MATRIX_GRID') +            config_h_lines.append('#   define SPLIT_HAND_MATRIX_GRID {%s}' % (','.join(kb_info_json["split"]["matrix_grid"],))) +            config_h_lines.append('#endif // SPLIT_HAND_MATRIX_GRID') +        elif kb_info_json['split']['primary'] == 'eeprom': +            config_h_lines.append('') +            config_h_lines.append('#ifndef EE_HANDS') +            config_h_lines.append('#   define EE_HANDS') +            config_h_lines.append('#endif // EE_HANDS') + +    if 'protocol' in kb_info_json['split'].get('transport', {}): +        if kb_info_json['split']['transport']['protocol'] == 'i2c': +            config_h_lines.append('') +            config_h_lines.append('#ifndef USE_I2C') +            config_h_lines.append('#   define USE_I2C') +            config_h_lines.append('#endif // USE_I2C') + +    if 'right' in kb_info_json['split'].get('matrix_pins', {}): +        config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_RIGHT')) + + [email protected]('-o', '--output', arg_only=True, type=normpath, help='File to write to') [email protected]('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate config.h for.') [email protected]('-km', '--keymap', arg_only=True, help='Keymap to generate config.h for.') [email protected]('Used by the make system to generate info_config.h from info.json', hidden=True) +def generate_config_h(cli): +    """Generates the info_config.h file. +    """ +    # Determine our keyboard/keymap +    if cli.args.keymap: +        km = locate_keymap(cli.args.keyboard, cli.args.keymap) +        km_json = json_load(km) +        validate(km_json, 'qmk.keymap.v1') +        kb_info_json = dotty(km_json.get('config', {})) +    else: +        kb_info_json = dotty(info_json(cli.args.keyboard)) + +    # Build the info_config.h file. +    config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once'] + +    generate_config_items(kb_info_json, config_h_lines) +      if 'matrix_pins' in kb_info_json:          config_h_lines.append(matrix_pins(kb_info_json['matrix_pins'])) +    if 'split' in kb_info_json: +        generate_split_config(kb_info_json, config_h_lines) +      # Show the results      config_h = '\n'.join(config_h_lines) diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index 8931b68b6f..284d1a8510 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py @@ -4,15 +4,17 @@ Compile an info.json for a particular keyboard and pretty-print it.  """  import json -from jsonschema import Draft7Validator, validators +from argcomplete.completers import FilesCompleter +from jsonschema import Draft7Validator, RefResolver, validators  from milc import cli +from pathlib import Path  from qmk.decorators import automagic_keyboard, automagic_keymap  from qmk.info import info_json  from qmk.json_encoders import InfoJSONEncoder -from qmk.json_schema import load_jsonschema +from qmk.json_schema import compile_schema_store  from qmk.keyboard import keyboard_completer, keyboard_folder -from qmk.path import is_keyboard +from qmk.path import is_keyboard, normpath  def pruning_validator(validator_class): @@ -34,15 +36,19 @@ def pruning_validator(validator_class):  def strip_info_json(kb_info_json):      """Remove the API-only properties from the info.json.      """ +    schema_store = compile_schema_store()      pruning_draft_7_validator = pruning_validator(Draft7Validator) -    schema = load_jsonschema('keyboard') -    validator = pruning_draft_7_validator(schema).validate +    schema = schema_store['qmk.keyboard.v1'] +    resolver = RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store) +    validator = pruning_draft_7_validator(schema, resolver=resolver).validate      return validator(kb_info_json)  @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.')  @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') [email protected]('-o', '--output', arg_only=True, completer=FilesCompleter, help='Write the output the specified file, overwriting if necessary.') [email protected]('-ow', '--overwrite', arg_only=True, action='store_true', help='Overwrite the existing info.json. (Overrides the location of --output)')  @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)  @automagic_keyboard  @automagic_keymap @@ -59,9 +65,29 @@ def generate_info_json(cli):          cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)          return False +    if cli.args.overwrite: +        output_path = (Path('keyboards') / cli.config.generate_info_json.keyboard / 'info.json').resolve() + +        if cli.args.output: +            cli.log.warning('Overwriting user supplied --output with %s', output_path) + +        cli.args.output = output_path +      # Build the info.json file      kb_info_json = info_json(cli.config.generate_info_json.keyboard)      strip_info_json(kb_info_json) +    info_json_text = json.dumps(kb_info_json, indent=4, cls=InfoJSONEncoder) + +    if cli.args.output: +        # Write to a file +        output_path = normpath(cli.args.output) + +        if output_path.exists(): +            cli.log.warning('Overwriting output file %s', output_path) + +        output_path.write_text(info_json_text + '\n') +        cli.log.info('Wrote info.json to %s.', output_path) -    # Display the results -    print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder)) +    else: +        # Display the results +        print(info_json_text) diff --git a/lib/python/qmk/cli/generate/keyboard_h.py b/lib/python/qmk/cli/generate/keyboard_h.py index 22500dbc91..c9d7f549b3 100755 --- a/lib/python/qmk/cli/generate/keyboard_h.py +++ b/lib/python/qmk/cli/generate/keyboard_h.py @@ -2,7 +2,6 @@  """  from milc import cli -from qmk.decorators import automagic_keyboard, automagic_keymap  from qmk.info import info_json  from qmk.keyboard import keyboard_completer, keyboard_folder  from qmk.path import normpath @@ -29,14 +28,12 @@ def would_populate_layout_h(keyboard):  @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')  @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.') [email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.')  @cli.subcommand('Used by the make system to generate keyboard.h from info.json', hidden=True) -@automagic_keyboard -@automagic_keymap  def generate_keyboard_h(cli):      """Generates the keyboard.h file.      """ -    has_layout_h = would_populate_layout_h(cli.config.generate_keyboard_h.keyboard) +    has_layout_h = would_populate_layout_h(cli.args.keyboard)      # Build the layouts.h file.      keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.' ' */', '', '#pragma once', '#include "quantum.h"'] diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 41c94e16b5..cdf17dfbcb 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -5,11 +5,11 @@ from pathlib import Path  from dotty_dict import dotty  from milc import cli -from qmk.decorators import automagic_keyboard, automagic_keymap  from qmk.info import info_json -from qmk.json_schema import json_load +from qmk.json_schema import json_load, validate  from qmk.keyboard import keyboard_completer, keyboard_folder -from qmk.path import is_keyboard, normpath +from qmk.keymap import locate_keymap +from qmk.path import normpath  def process_mapping_rule(kb_info_json, rules_key, info_dict): @@ -39,23 +39,21 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):  @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')  @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")  @cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode") [email protected]('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.') [email protected]('Used by the make system to generate info_config.h from info.json', hidden=True) -@automagic_keyboard -@automagic_keymap [email protected]('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate rules.mk for.') [email protected]('-km', '--keymap', arg_only=True, help='Keymap to generate rules.mk for.') [email protected]('Used by the make system to generate rules.mk from info.json', hidden=True)  def generate_rules_mk(cli):      """Generates a rules.mk file from info.json.      """ -    if not cli.config.generate_rules_mk.keyboard: -        cli.log.error('Missing parameter: --keyboard') -        cli.subcommands['info'].print_help() -        return False - -    if not is_keyboard(cli.config.generate_rules_mk.keyboard): -        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard) -        return False +    # Determine our keyboard/keymap +    if cli.args.keymap: +        km = locate_keymap(cli.args.keyboard, cli.args.keymap) +        km_json = json_load(km) +        validate(km_json, 'qmk.keymap.v1') +        kb_info_json = dotty(km_json.get('config', {})) +    else: +        kb_info_json = dotty(info_json(cli.args.keyboard)) -    kb_info_json = dotty(info_json(cli.config.generate_rules_mk.keyboard))      info_rules_map = json_load(Path('data/mappings/info_rules.json'))      rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', ''] @@ -76,6 +74,17 @@ def generate_rules_mk(cli):                  enabled = 'yes' if enabled else 'no'                  rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') +    # Set SPLIT_TRANSPORT, if needed +    if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': +        rules_mk_lines.append('SPLIT_TRANSPORT ?= custom') + +    # Set CUSTOM_MATRIX, if needed +    if kb_info_json.get('matrix_pins', {}).get('custom'): +        if kb_info_json.get('matrix_pins', {}).get('custom_lite'): +            rules_mk_lines.append('CUSTOM_MATRIX ?= lite') +        else: +            rules_mk_lines.append('CUSTOM_MATRIX ?= yes') +      # Show the results      rules_mk = '\n'.join(rules_mk_lines) + '\n' diff --git a/lib/python/qmk/cli/generate/version_h.py b/lib/python/qmk/cli/generate/version_h.py new file mode 100644 index 0000000000..b8e52588c4 --- /dev/null +++ b/lib/python/qmk/cli/generate/version_h.py @@ -0,0 +1,28 @@ +"""Used by the make system to generate version.h for use in code. +""" +from milc import cli + +from qmk.commands import create_version_h +from qmk.path import normpath + + [email protected]('-o', '--output', arg_only=True, type=normpath, help='File to write to') [email protected]('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") [email protected]('--skip-git', arg_only=True, action='store_true', help='Skip Git operations') [email protected]('--skip-all', arg_only=True, action='store_true', help='Use placeholder values for all defines (implies --skip-git)') [email protected]('Used by the make system to generate version.h for use in code', hidden=True) +def generate_version_h(cli): +    """Generates the version.h file. +    """ +    if cli.args.skip_all: +        cli.args.skip_git = True + +    version_h = create_version_h(cli.args.skip_git, cli.args.skip_all) + +    if cli.args.output: +        cli.args.output.write_text(version_h) + +        if not cli.args.quiet: +            cli.log.info('Wrote version.h to %s.', cli.args.output) +    else: +        print(version_h) diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index 0d08d242cd..3131d4b53f 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -24,19 +24,15 @@ def show_keymap(kb_info_json, title_caps=True):      keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)      if keymap_path and keymap_path.suffix == '.json': -        if title_caps: -            cli.echo('{fg_blue}Keymap "%s"{fg_reset}:', cli.config.info.keymap) -        else: -            cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap) -          keymap_data = json.load(keymap_path.open(encoding='utf-8'))          layout_name = keymap_data['layout'] +        layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name)  # Resolve alias names          for layer_num, layer in enumerate(keymap_data['layers']):              if title_caps: -                cli.echo('{fg_cyan}Layer %s{fg_reset}:', layer_num) +                cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num)              else: -                cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num) +                cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num)              print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer)) @@ -45,7 +41,7 @@ def show_layouts(kb_info_json, title_caps=True):      """Render the layouts with info.json labels.      """      for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items(): -        title = layout_name.title() if title_caps else layout_name +        title = f'Layout {layout_name.title()}' if title_caps else f'layouts.{layout_name}'          cli.echo('{fg_cyan}%s{fg_reset}:', title)          print(layout_art)  # Avoid passing dirty data to cli.echo() @@ -87,23 +83,12 @@ def print_friendly_output(kb_info_json):          cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])      cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))      cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys()))) -    if 'width' in kb_info_json and 'height' in kb_info_json: -        cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))      cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))      cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))      if 'layout_aliases' in kb_info_json:          aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]          cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),)) -    if cli.config.info.layouts: -        show_layouts(kb_info_json, True) - -    if cli.config.info.matrix: -        show_matrix(kb_info_json, True) - -    if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file': -        show_keymap(kb_info_json, True) -  def print_text_output(kb_info_json):      """Print the info.json in a plain text format. @@ -124,6 +109,24 @@ def print_text_output(kb_info_json):          show_keymap(kb_info_json, False) +def print_dotted_output(kb_info_json, prefix=''): +    """Print the info.json in a plain text format with dot-joined keys. +    """ +    for key in sorted(kb_info_json): +        new_prefix = f'{prefix}.{key}' if prefix else key + +        if key in ['parse_errors', 'parse_warnings']: +            continue +        elif key == 'layouts' and prefix == '': +            cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys()))) +        elif isinstance(kb_info_json[key], dict): +            print_dotted_output(kb_info_json[key], new_prefix) +        elif isinstance(kb_info_json[key], list): +            cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, sorted(kb_info_json[key])))) +        else: +            cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key]) + +  def print_parsed_rules_mk(keyboard_name):      rules = rules_mk(keyboard_name)      for k in sorted(rules.keys()): @@ -164,10 +167,22 @@ def info(cli):      # Output in the requested format      if cli.args.format == 'json':          print(json.dumps(kb_info_json, cls=InfoJSONEncoder)) +        return True      elif cli.args.format == 'text': -        print_text_output(kb_info_json) +        print_dotted_output(kb_info_json) +        title_caps = False      elif cli.args.format == 'friendly':          print_friendly_output(kb_info_json) +        title_caps = True      else:          cli.log.error('Unknown format: %s', cli.args.format)          return False + +    if cli.config.info.layouts: +        show_layouts(kb_info_json, title_caps) + +    if cli.config.info.matrix: +        show_matrix(kb_info_json, title_caps) + +    if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file': +        show_keymap(kb_info_json, title_caps) diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py index acb75ef4fd..bbfddf4268 100755 --- a/lib/python/qmk/cli/kle2json.py +++ b/lib/python/qmk/cli/kle2json.py @@ -44,8 +44,6 @@ def kle2json(cli):          'keyboard_name': kle.name,          'url': '',          'maintainer': 'qmk', -        'width': kle.columns, -        'height': kle.rows,          'layouts': {              'LAYOUT': {                  'layout': kle2qmk(kle) diff --git a/lib/python/qmk/cli/multibuild.py b/lib/python/qmk/cli/multibuild.py index bdb0b493c8..85ed0fa1e9 100755 --- a/lib/python/qmk/cli/multibuild.py +++ b/lib/python/qmk/cli/multibuild.py @@ -10,7 +10,7 @@ from subprocess import DEVNULL  from milc import cli  from qmk.constants import QMK_FIRMWARE -from qmk.commands import _find_make +from qmk.commands import _find_make, get_make_parallel_args  import qmk.keyboard  import qmk.keymap @@ -28,7 +28,7 @@ def _is_split(keyboard_name):      return True if 'SPLIT_KEYBOARD' in rules_mk and rules_mk['SPLIT_KEYBOARD'].lower() == 'yes' else False [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.") [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")  @cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")  @cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.")  @cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.") @@ -80,7 +80,7 @@ all: {keyboard_safe}_binary                  )                  # yapf: enable -    cli.run([make_cmd, '-j', str(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL) +    cli.run([make_cmd, *get_make_parallel_args(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)      # Check for failures      failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')] diff --git a/lib/python/qmk/cli/new/keyboard.py b/lib/python/qmk/cli/new/keyboard.py index ae4445ca48..9e4232679d 100644 --- a/lib/python/qmk/cli/new/keyboard.py +++ b/lib/python/qmk/cli/new/keyboard.py @@ -1,11 +1,142 @@ -"""This script automates the creation of keyboards. +"""This script automates the creation of new keyboard directories using a starter template.  """ +from datetime import date +import fileinput +from pathlib import Path +import re +import shutil + +from qmk.commands import git_get_username +import qmk.path  from milc import cli +from milc.questions import choice, question + +KEYBOARD_TYPES = ['avr', 'ps2avrgb'] + + +def keyboard_name(name): +    """Callable for argparse validation. +    """ +    if not validate_keyboard_name(name): +        raise ValueError +    return name [email protected]('Creates a new keyboard') +def validate_keyboard_name(name): +    """Returns True if the given keyboard name contains only lowercase a-z, 0-9 and underscore characters. +    """ +    regex = re.compile(r'^[a-z0-9][a-z0-9/_]+$') +    return bool(regex.match(name)) + + [email protected]('-kb', '--keyboard', help='Specify the name for the new keyboard directory', arg_only=True, type=keyboard_name) [email protected]('-t', '--type', help='Specify the keyboard type', arg_only=True, choices=KEYBOARD_TYPES) [email protected]('-u', '--username', help='Specify your username (default from Git config)', arg_only=True) [email protected]('Creates a new keyboard directory')  def new_keyboard(cli): -    """Creates a new keyboard +    """Creates a new keyboard.      """ -    # TODO: replace this bodge to the existing script -    cli.run(['util/new_keyboard.sh'], stdin=None, capture_output=False) +    cli.log.info('{style_bright}Generating a new QMK keyboard directory{style_normal}') +    cli.echo('') + +    # Get keyboard name +    new_keyboard_name = None +    while not new_keyboard_name: +        new_keyboard_name = cli.args.keyboard if cli.args.keyboard else question('Keyboard Name:') +        if not validate_keyboard_name(new_keyboard_name): +            cli.log.error('Keyboard names must contain only {fg_cyan}lowercase a-z{fg_reset}, {fg_cyan}0-9{fg_reset}, and {fg_cyan}_{fg_reset}! Please choose a different name.') + +            # Exit if passed by arg +            if cli.args.keyboard: +                return False + +            new_keyboard_name = None +            continue + +        keyboard_path = qmk.path.keyboard(new_keyboard_name) +        if keyboard_path.exists(): +            cli.log.error(f'Keyboard {{fg_cyan}}{new_keyboard_name}{{fg_reset}} already exists! Please choose a different name.') + +            # Exit if passed by arg +            if cli.args.keyboard: +                return False + +            new_keyboard_name = None + +    # Get keyboard type +    keyboard_type = cli.args.type if cli.args.type else choice('Keyboard Type:', KEYBOARD_TYPES, default=0) + +    # Get username +    user_name = None +    while not user_name: +        user_name = question('Your Name:', default=find_user_name()) + +        if not user_name: +            cli.log.error('You didn\'t provide a username, and we couldn\'t find one set in your QMK or Git configs. Please try again.') + +            # Exit if passed by arg +            if cli.args.username: +                return False + +    # Copy all the files +    copy_templates(keyboard_type, keyboard_path) + +    # Replace all the placeholders +    keyboard_basename = keyboard_path.name +    replacements = [ +        ('%YEAR%', str(date.today().year)), +        ('%KEYBOARD%', keyboard_basename), +        ('%YOUR_NAME%', user_name), +    ] +    filenames = [ +        keyboard_path / 'config.h', +        keyboard_path / 'info.json', +        keyboard_path / 'readme.md', +        keyboard_path / f'{keyboard_basename}.c', +        keyboard_path / f'{keyboard_basename}.h', +        keyboard_path / 'keymaps/default/readme.md', +        keyboard_path / 'keymaps/default/keymap.c', +    ] +    replace_placeholders(replacements, filenames) + +    cli.echo('') +    cli.log.info(f'{{fg_green}}Created a new keyboard called {{fg_cyan}}{new_keyboard_name}{{fg_green}}.{{fg_reset}}') +    cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}{keyboard_path}{{fg_reset}},') +    cli.log.info('or open the directory in your preferred text editor.') + + +def find_user_name(): +    if cli.args.username: +        return cli.args.username +    elif cli.config.user.name: +        return cli.config.user.name +    else: +        return git_get_username() + + +def copy_templates(keyboard_type, keyboard_path): +    """Copies the template files from quantum/template to the new keyboard directory. +    """ +    template_base_path = Path('quantum/template') +    keyboard_basename = keyboard_path.name + +    cli.log.info('Copying base template files...') +    shutil.copytree(template_base_path / 'base', keyboard_path) + +    cli.log.info(f'Copying {{fg_cyan}}{keyboard_type}{{fg_reset}} template files...') +    shutil.copytree(template_base_path / keyboard_type, keyboard_path, dirs_exist_ok=True) + +    cli.log.info(f'Renaming {{fg_cyan}}keyboard.[ch]{{fg_reset}} to {{fg_cyan}}{keyboard_basename}.[ch]{{fg_reset}}...') +    shutil.move(keyboard_path / 'keyboard.c', keyboard_path / f'{keyboard_basename}.c') +    shutil.move(keyboard_path / 'keyboard.h', keyboard_path / f'{keyboard_basename}.h') + + +def replace_placeholders(replacements, filenames): +    """Replaces the given placeholders in each template file. +    """ +    for replacement in replacements: +        cli.log.info(f'Replacing {{fg_cyan}}{replacement[0]}{{fg_reset}} with {{fg_cyan}}{replacement[1]}{{fg_reset}}...') + +        with fileinput.input(files=filenames, inplace=True) as file: +            for line in file: +                print(line.replace(replacement[0], replacement[1]), end='') diff --git a/lib/python/qmk/cli/pyformat.py b/lib/python/qmk/cli/pyformat.py index abe5f6de19..c624f74aeb 100755 --- a/lib/python/qmk/cli/pyformat.py +++ b/lib/python/qmk/cli/pyformat.py @@ -1,26 +1,24 @@ -"""Format python code according to QMK's style. +"""Point people to the new command name.  """ -from subprocess import CalledProcessError, DEVNULL +import sys +from pathlib import Path  from milc import cli [email protected]('-n', '--dry-run', arg_only=True, action='store_true', help="Flag only, don't automatically format.") [email protected]("Format python code according to QMK's style.", hidden=False if cli.config.user.developer else True) [email protected]('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually format.") [email protected]('Pointer to the new command name: qmk format-python.', hidden=False if cli.config.user.developer else True)  def pyformat(cli): -    """Format python code according to QMK's style. +    """Pointer to the new command name: qmk format-python.      """ -    edit = '--diff' if cli.args.dry_run else '--in-place' -    yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python'] -    try: -        cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) -        cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.') -        return True +    cli.log.warning('"qmk pyformat" has been renamed to "qmk format-python". Please use the new command in the future.') +    argv = [sys.executable, *sys.argv] +    argv[argv.index('pyformat')] = 'format-python' +    script_path = Path(argv[1]) +    script_path_exe = Path(f'{argv[1]}.exe') -    except CalledProcessError: -        if cli.args.dry_run: -            cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!') -        else: -            cli.log.error('Error formatting python code!') +    if not script_path.exists() and script_path_exe.exists(): +        # For reasons I don't understand ".exe" is stripped from the script name on windows. +        argv[1] = str(script_path_exe) -    return False +    return cli.run(argv, capture_output=False).returncode diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 3a35c11031..421453d837 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -2,6 +2,7 @@  """  import json  import os +import sys  import shutil  from pathlib import Path  from subprocess import DEVNULL @@ -10,7 +11,7 @@ from time import strftime  from milc import cli  import qmk.keymap -from qmk.constants import KEYBOARD_OUTPUT_PREFIX +from qmk.constants import QMK_FIRMWARE, KEYBOARD_OUTPUT_PREFIX  from qmk.json_schema import json_load  time_fmt = '%Y-%m-%d-%H:%M:%S' @@ -51,7 +52,7 @@ def create_make_target(target, parallel=1, **env_vars):      for key, value in env_vars.items():          env.append(f'{key}={value}') -    return [make_cmd, '-j', str(parallel), *env, target] +    return [make_cmd, *get_make_parallel_args(parallel), *env, target]  def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars): @@ -86,11 +87,17 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):      return create_make_target(':'.join(make_args), parallel, **env_vars) -def get_git_version(repo_dir='.', check_dir='.'): +def get_git_version(current_time, repo_dir='.', check_dir='.'):      """Returns the current git version for a repo, or the current time.      """      git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags'] +    if repo_dir != '.': +        repo_dir = Path('lib') / repo_dir + +    if check_dir != '.': +        check_dir = repo_dir / check_dir +      if Path(check_dir).exists():          git_describe = cli.run(git_describe_cmd, stdin=DEVNULL, cwd=repo_dir) @@ -100,23 +107,58 @@ def get_git_version(repo_dir='.', check_dir='.'):          else:              cli.log.warn(f'"{" ".join(git_describe_cmd)}" returned error code {git_describe.returncode}')              print(git_describe.stderr) -            return strftime(time_fmt) +            return current_time + +    return current_time + + +def get_make_parallel_args(parallel=1): +    """Returns the arguments for running the specified number of parallel jobs. +    """ +    parallel_args = [] + +    if int(parallel) <= 0: +        # 0 or -1 means -j without argument (unlimited jobs) +        parallel_args.append('--jobs') +    else: +        parallel_args.append('--jobs=' + str(parallel)) + +    if int(parallel) != 1: +        # If more than 1 job is used, synchronize parallel output by target +        parallel_args.append('--output-sync=target') -    return strftime(time_fmt) +    return parallel_args -def write_version_h(git_version, build_date, chibios_version, chibios_contrib_version): -    """Generate and write quantum/version.h +def create_version_h(skip_git=False, skip_all=False): +    """Generate version.h contents      """ -    version_h = [ -        f'#define QMK_VERSION "{git_version}"', -        f'#define QMK_BUILDDATE "{build_date}"', -        f'#define CHIBIOS_VERSION "{chibios_version}"', -        f'#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}"', -    ] +    if skip_all: +        current_time = "1970-01-01-00:00:00" +    else: +        current_time = strftime(time_fmt) + +    if skip_git: +        git_version = "NA" +        chibios_version = "NA" +        chibios_contrib_version = "NA" +    else: +        git_version = get_git_version(current_time) +        chibios_version = get_git_version(current_time, "chibios", "os") +        chibios_contrib_version = get_git_version(current_time, "chibios-contrib", "os") + +    version_h_lines = f"""/* This file was automatically generated. Do not edit or copy. + */ + +#pragma once + +#define QMK_VERSION "{git_version}" +#define QMK_BUILDDATE "{current_time}" +#define CHIBIOS_VERSION "{chibios_version}" +#define CHIBIOS_CONTRIB_VERSION "{chibios_contrib_version}" +""" -    version_h_file = Path('quantum/version.h') -    version_h_file.write_text('\n'.join(version_h)) +    return version_h_lines  def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_vars): @@ -149,13 +191,8 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va      keymap_dir.mkdir(exist_ok=True, parents=True)      keymap_c.write_text(c_text) -    # Write the version.h file -    git_version = get_git_version() -    build_date = strftime('%Y-%m-%d-%H:%M:%S') -    chibios_version = get_git_version("lib/chibios", "lib/chibios/os") -    chibios_contrib_version = get_git_version("lib/chibios-contrib", "lib/chibios-contrib/os") - -    write_version_h(git_version, build_date, chibios_version, chibios_contrib_version) +    version_h = Path('quantum/version.h') +    version_h.write_text(create_version_h())      # Return a command that can be run to make the keymap and flash if given      verbose = 'true' if cli.config.general.verbose else 'false' @@ -166,8 +203,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va          make_command.append('-s')      make_command.extend([ -        '-j', -        str(parallel), +        *get_make_parallel_args(parallel),          '-r',          '-R',          '-f', @@ -181,10 +217,6 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va          make_command.append(f'{key}={value}')      make_command.extend([ -        f'GIT_VERSION={git_version}', -        f'BUILD_DATE={build_date}', -        f'CHIBIOS_VERSION={chibios_version}', -        f'CHIBIOS_CONTRIB_VERSION={chibios_contrib_version}',          f'KEYBOARD={user_keymap["keyboard"]}',          f'KEYMAP={user_keymap["keymap"]}',          f'KEYBOARD_FILESAFE={keyboard_filesafe}', @@ -223,3 +255,80 @@ def parse_configurator_json(configurator_file):              user_keymap['layout'] = aliases[orig_keyboard]['layouts'][user_keymap['layout']]      return user_keymap + + +def git_get_username(): +    """Retrieves user's username from Git config, if set. +    """ +    git_username = cli.run(['git', 'config', '--get', 'user.name']) + +    if git_username.returncode == 0 and git_username.stdout: +        return git_username.stdout.strip() + + +def git_check_repo(): +    """Checks that the .git directory exists inside QMK_HOME. + +    This is a decent enough indicator that the qmk_firmware directory is a +    proper Git repository, rather than a .zip download from GitHub. +    """ +    dot_git_dir = QMK_FIRMWARE / '.git' + +    return dot_git_dir.is_dir() + + +def git_get_branch(): +    """Returns the current branch for a repo, or None. +    """ +    git_branch = cli.run(['git', 'branch', '--show-current']) +    if not git_branch.returncode != 0 or not git_branch.stdout: +        # Workaround for Git pre-2.22 +        git_branch = cli.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) + +    if git_branch.returncode == 0: +        return git_branch.stdout.strip() + + +def git_is_dirty(): +    """Returns 1 if repo is dirty, or 0 if clean +    """ +    git_diff_staged_cmd = ['git', 'diff', '--quiet'] +    git_diff_unstaged_cmd = [*git_diff_staged_cmd, '--cached'] + +    unstaged = cli.run(git_diff_staged_cmd) +    staged = cli.run(git_diff_unstaged_cmd) + +    return unstaged.returncode != 0 or staged.returncode != 0 + + +def git_get_remotes(): +    """Returns the current remotes for a repo. +    """ +    remotes = {} + +    git_remote_show_cmd = ['git', 'remote', 'show'] +    git_remote_get_cmd = ['git', 'remote', 'get-url'] + +    git_remote_show = cli.run(git_remote_show_cmd) +    if git_remote_show.returncode == 0: +        for name in git_remote_show.stdout.splitlines(): +            git_remote_name = cli.run([*git_remote_get_cmd, name]) +            remotes[name.strip()] = {"url": git_remote_name.stdout.strip()} + +    return remotes + + +def git_check_deviation(active_branch): +    """Return True if branch has custom commits +    """ +    cli.run(['git', 'fetch', 'upstream', active_branch]) +    deviations = cli.run(['git', '--no-pager', 'log', f'upstream/{active_branch}...{active_branch}']) +    return bool(deviations.returncode) + + +def in_virtualenv(): +    """Check if running inside a virtualenv. +    Based on https://stackoverflow.com/a/1883251 +    """ +    active_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix +    return active_prefix != sys.prefix diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 49e5e0eb42..71a6c91c77 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -6,11 +6,14 @@ from pathlib import Path  # The root of the qmk_firmware tree.  QMK_FIRMWARE = Path.cwd() +# Upstream repo url +QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware' +  # This is the number of directories under `qmk_firmware/keyboards` that will be traversed. This is currently a limitation of our make system.  MAX_KEYBOARD_SUBFOLDERS = 5  # Supported processor types -CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443' +CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443'  LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None  VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index d23b3592ee..b6f2ecf644 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -9,7 +9,7 @@ from milc import cli  from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS  from qmk.c_parse import find_layouts -from qmk.json_schema import deep_update, json_load, keyboard_validate, keyboard_api_validate +from qmk.json_schema import deep_update, json_load, validate  from qmk.keyboard import config_h, rules_mk  from qmk.keymap import list_keymaps  from qmk.makefile import parse_rules_mk_file @@ -49,7 +49,7 @@ def info_json(keyboard):          info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}      # Populate layout data -    layouts, aliases = _find_all_layouts(info_data, keyboard) +    layouts, aliases = _search_keyboard_h(keyboard)      if aliases:          info_data['layout_aliases'] = aliases @@ -61,12 +61,15 @@ def info_json(keyboard):      # Merge in the data from info.json, config.h, and rules.mk      info_data = merge_info_jsons(keyboard, info_data) -    info_data = _extract_config_h(info_data)      info_data = _extract_rules_mk(info_data) +    info_data = _extract_config_h(info_data) + +    # Ensure that we have matrix row and column counts +    info_data = _matrix_size(info_data)      # Validate against the jsonschema      try: -        keyboard_api_validate(info_data) +        validate(info_data, 'qmk.api.keyboard.v1')      except jsonschema.ValidationError as e:          json_path = '.'.join([str(p) for p in e.absolute_path]) @@ -75,6 +78,9 @@ def info_json(keyboard):      # Make sure we have at least one layout      if not info_data.get('layouts'): +        _find_missing_layouts(info_data, keyboard) + +    if not info_data.get('layouts'):          _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')      # Filter out any non-existing community layouts @@ -90,6 +96,9 @@ def info_json(keyboard):          if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}):              _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name)) +    # Check that the reported matrix size is consistent with the actual matrix size +    _check_matrix(info_data) +      return info_data @@ -143,10 +152,7 @@ def _pin_name(pin):      elif pin == 'NO_PIN':          return None -    elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit(): -        return pin - -    raise ValueError(f'Invalid pin: {pin}') +    return pin  def _extract_pins(pins): @@ -155,10 +161,9 @@ def _extract_pins(pins):      return [_pin_name(pin) for pin in pins.split(',')] -def _extract_direct_matrix(info_data, direct_pins): +def _extract_direct_matrix(direct_pins):      """      """ -    info_data['matrix_pins'] = {}      direct_pin_array = []      while direct_pins[-1] != '}': @@ -182,12 +187,157 @@ def _extract_direct_matrix(info_data, direct_pins):      return direct_pin_array +def _extract_audio(info_data, config_c): +    """Populate data about the audio configuration +    """ +    audio_pins = [] + +    for pin in 'B5', 'B6', 'B7', 'C4', 'C5', 'C6': +        if config_c.get(f'{pin}_AUDIO'): +            audio_pins.append(pin) + +    if audio_pins: +        info_data['audio'] = {'pins': audio_pins} + + +def _extract_split_main(info_data, config_c): +    """Populate data about the split configuration +    """ +    # Figure out how the main half is determined +    if config_c.get('SPLIT_HAND_PIN') is True: +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'main' in info_data['split']: +            _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_PIN) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) + +        info_data['split']['main'] = 'pin' + +    if config_c.get('SPLIT_HAND_MATRIX_GRID'): +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'main' in info_data['split']: +            _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_MATRIX_GRID) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) + +        info_data['split']['main'] = 'matrix_grid' +        info_data['split']['matrix_grid'] = _extract_pins(config_c['SPLIT_HAND_MATRIX_GRID']) + +    if config_c.get('EE_HANDS') is True: +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'main' in info_data['split']: +            _log_warning(info_data, 'Split main hand is specified in both config.h (EE_HANDS) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) + +        info_data['split']['main'] = 'eeprom' + +    if config_c.get('MASTER_RIGHT') is True: +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'main' in info_data['split']: +            _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_RIGHT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) + +        info_data['split']['main'] = 'right' + +    if config_c.get('MASTER_LEFT') is True: +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'main' in info_data['split']: +            _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_LEFT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main']) + +        info_data['split']['main'] = 'left' + + +def _extract_split_transport(info_data, config_c): +    # Figure out the transport method +    if config_c.get('USE_I2C') is True: +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'transport' not in info_data['split']: +            info_data['split']['transport'] = {} + +        if 'protocol' in info_data['split']['transport']: +            _log_warning(info_data, 'Split transport is specified in both config.h (USE_I2C) and info.json (split.transport.protocol) (Value: %s), the config.h value wins.' % info_data['split']['transport']) + +        info_data['split']['transport']['protocol'] = 'i2c' + +    elif 'protocol' not in info_data.get('split', {}).get('transport', {}): +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'transport' not in info_data['split']: +            info_data['split']['transport'] = {} + +        info_data['split']['transport']['protocol'] = 'serial' + + +def _extract_split_right_pins(info_data, config_c): +    # Figure out the right half matrix pins +    row_pins = config_c.get('MATRIX_ROW_PINS_RIGHT', '').replace('{', '').replace('}', '').strip() +    col_pins = config_c.get('MATRIX_COL_PINS_RIGHT', '').replace('{', '').replace('}', '').strip() +    unused_pin_text = config_c.get('UNUSED_PINS_RIGHT') +    unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None +    direct_pins = config_c.get('DIRECT_PINS_RIGHT', '').replace(' ', '')[1:-1] + +    if row_pins and col_pins: +        if info_data.get('split', {}).get('matrix_pins', {}).get('right') in info_data: +            _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.') + +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'matrix_pins' not in info_data['split']: +            info_data['split']['matrix_pins'] = {} + +        if 'right' not in info_data['split']['matrix_pins']: +            info_data['split']['matrix_pins']['right'] = {} + +        info_data['split']['matrix_pins']['right'] = { +            'cols': _extract_pins(col_pins), +            'rows': _extract_pins(row_pins), +        } + +    if direct_pins: +        if info_data.get('split', {}).get('matrix_pins', {}).get('right', {}): +            _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.') + +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'matrix_pins' not in info_data['split']: +            info_data['split']['matrix_pins'] = {} + +        if 'right' not in info_data['split']['matrix_pins']: +            info_data['split']['matrix_pins']['right'] = {} + +        info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins) + +    if unused_pins: +        if 'split' not in info_data: +            info_data['split'] = {} + +        if 'matrix_pins' not in info_data['split']: +            info_data['split']['matrix_pins'] = {} + +        if 'right' not in info_data['split']['matrix_pins']: +            info_data['split']['matrix_pins']['right'] = {} + +        info_data['split']['matrix_pins']['right']['unused'] = _extract_pins(unused_pins) + +  def _extract_matrix_info(info_data, config_c):      """Populate the matrix information.      """      row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()      col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() +    unused_pin_text = config_c.get('UNUSED_PINS') +    unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None      direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] +    info_snippet = {}      if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:          if 'matrix_size' in info_data: @@ -199,19 +349,35 @@ def _extract_matrix_info(info_data, config_c):          }      if row_pins and col_pins: -        if 'matrix_pins' in info_data: +        if 'matrix_pins' in info_data and 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:              _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.') -        info_data['matrix_pins'] = { -            'cols': _extract_pins(col_pins), -            'rows': _extract_pins(row_pins), -        } +        info_snippet['cols'] = _extract_pins(col_pins) +        info_snippet['rows'] = _extract_pins(row_pins)      if direct_pins: -        if 'matrix_pins' in info_data: +        if 'matrix_pins' in info_data and 'direct' in info_data['matrix_pins']:              _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.') -        info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins) +        info_snippet['direct'] = _extract_direct_matrix(direct_pins) + +    if unused_pins: +        if 'matrix_pins' not in info_data: +            info_data['matrix_pins'] = {} + +        info_snippet['unused'] = _extract_pins(unused_pins) + +    if config_c.get('CUSTOM_MATRIX', 'no') != 'no': +        if 'matrix_pins' in info_data and 'custom' in info_data['matrix_pins']: +            _log_warning(info_data, 'Custom Matrix is specified in both info.json and config.h, the config.h values win.') + +        info_snippet['custom'] = True + +        if config_c['CUSTOM_MATRIX'] == 'lite': +            info_snippet['custom_lite'] = True + +    if info_snippet: +        info_data['matrix_pins'] = info_snippet      return info_data @@ -269,6 +435,10 @@ def _extract_config_h(info_data):      # Pull data that easily can't be mapped in json      _extract_matrix_info(info_data, config_c) +    _extract_audio(info_data, config_c) +    _extract_split_main(info_data, config_c) +    _extract_split_transport(info_data, config_c) +    _extract_split_right_pins(info_data, config_c)      return info_data @@ -341,6 +511,46 @@ def _extract_rules_mk(info_data):      return info_data +def _matrix_size(info_data): +    """Add info_data['matrix_size'] if it doesn't exist. +    """ +    if 'matrix_size' not in info_data and 'matrix_pins' in info_data: +        info_data['matrix_size'] = {} + +        if 'direct' in info_data['matrix_pins']: +            info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0]) +            info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct']) +        elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']: +            info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols']) +            info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows']) + +    return info_data + + +def _check_matrix(info_data): +    """Check the matrix to ensure that row/column count is consistent. +    """ +    if 'matrix_pins' in info_data and 'matrix_size' in info_data: +        actual_col_count = info_data['matrix_size'].get('cols', 0) +        actual_row_count = info_data['matrix_size'].get('rows', 0) +        col_count = row_count = 0 + +        if 'direct' in info_data['matrix_pins']: +            col_count = len(info_data['matrix_pins']['direct'][0]) +            row_count = len(info_data['matrix_pins']['direct']) +        elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']: +            col_count = len(info_data['matrix_pins']['cols']) +            row_count = len(info_data['matrix_pins']['rows']) + +        if col_count != actual_col_count and col_count != (actual_col_count / 2): +            # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check. +            _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}') + +        if row_count != actual_row_count and row_count != (actual_row_count / 2): +            # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check. +            _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}') + +  def _merge_layouts(info_data, new_info_data):      """Merge new_info_data into info_data in an intelligent way.      """ @@ -374,12 +584,13 @@ def _merge_layouts(info_data, new_info_data):      return info_data -def _search_keyboard_h(path): +def _search_keyboard_h(keyboard): +    keyboard = Path(keyboard)      current_path = Path('keyboards/')      aliases = {}      layouts = {} -    for directory in path.parts: +    for directory in keyboard.parts:          current_path = current_path / directory          keyboard_h = '%s.h' % (directory,)          keyboard_h_path = current_path / keyboard_h @@ -394,27 +605,28 @@ def _search_keyboard_h(path):      return layouts, aliases -def _find_all_layouts(info_data, keyboard): -    """Looks for layout macros associated with this keyboard. -    """ -    layouts, aliases = _search_keyboard_h(Path(keyboard)) +def _find_missing_layouts(info_data, keyboard): +    """Looks for layout macros when they aren't found other places. -    if not layouts: -        # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above. -        info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard)) +    If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above. +    """ +    _log_warning(info_data, '%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard)) -        for file in glob('keyboards/%s/*.h' % keyboard): -            if file.endswith('.h'): -                these_layouts, these_aliases = find_layouts(file) +    for file in glob('keyboards/%s/*.h' % keyboard): +        these_layouts, these_aliases = find_layouts(file) -                if these_layouts: -                    layouts.update(these_layouts) +        if these_layouts: +            for layout_name, layout_json in these_layouts.items(): +                if not layout_name.startswith('LAYOUT_kc'): +                    layout_json['c_macro'] = True +                    info_data['layouts'][layout_name] = layout_json -                for alias, alias_text in these_aliases.items(): -                    if alias_text in layouts: -                        aliases[alias] = alias_text +        for alias, alias_text in these_aliases.items(): +            if alias_text in these_layouts: +                if 'layout_aliases' not in info_data: +                    info_data['layout_aliases'] = {} -    return layouts, aliases +                info_data['layout_aliases'][alias] = alias_text  def _log_error(info_data, message): @@ -493,7 +705,7 @@ def merge_info_jsons(keyboard, info_data):              continue          try: -            keyboard_validate(new_info_data) +            validate(new_info_data, 'qmk.keyboard.v1')          except jsonschema.ValidationError as e:              json_path = '.'.join([str(p) for p in e.absolute_path])              cli.log.error('Not including data from file: %s', info_file) diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 9f3da022b4..72e91973a3 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py @@ -102,9 +102,6 @@ class InfoJSONEncoder(QMKJSONEncoder):              elif key == 'maintainer':                  return '12maintainer' -            elif key in ('height', 'width'): -                return '40' + str(key) -              elif key == 'community_layouts':                  return '97community_layouts' diff --git a/lib/python/qmk/json_schema.py b/lib/python/qmk/json_schema.py index f3992ee71a..ffc7c6bcd1 100644 --- a/lib/python/qmk/json_schema.py +++ b/lib/python/qmk/json_schema.py @@ -2,6 +2,7 @@  """  import json  from collections.abc import Mapping +from functools import lru_cache  from pathlib import Path  import hjson @@ -25,11 +26,13 @@ def json_load(json_file):          exit(1) +@lru_cache(maxsize=0)  def load_jsonschema(schema_name):      """Read a jsonschema file from disk. - -    FIXME(skullydazed/anyone): Refactor to make this a public function.      """ +    if Path(schema_name).exists(): +        return json_load(schema_name) +      schema_path = Path(f'data/schemas/{schema_name}.jsonschema')      if not schema_path.exists(): @@ -38,28 +41,42 @@ def load_jsonschema(schema_name):      return json_load(schema_path) -def keyboard_validate(data): -    """Validates data against the keyboard jsonschema. +@lru_cache(maxsize=0) +def compile_schema_store(): +    """Compile all our schemas into a schema store.      """ -    schema = load_jsonschema('keyboard') -    validator = jsonschema.Draft7Validator(schema).validate +    schema_store = {} -    return validator(data) +    for schema_file in Path('data/schemas').glob('*.jsonschema'): +        schema_data = load_jsonschema(schema_file) +        if not isinstance(schema_data, dict): +            cli.log.debug('Skipping schema file %s', schema_file) +            continue +        schema_store[schema_data['$id']] = schema_data + +    return schema_store + + +@lru_cache(maxsize=0) +def create_validator(schema): +    """Creates a validator for the given schema id. +    """ +    schema_store = compile_schema_store() +    resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store) + +    return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate -def keyboard_api_validate(data): -    """Validates data against the api_keyboard jsonschema. +def validate(data, schema): +    """Validates data against a schema.      """ -    base = load_jsonschema('keyboard') -    relative = load_jsonschema('api_keyboard') -    resolver = jsonschema.RefResolver.from_schema(base) -    validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate +    validator = create_validator(schema)      return validator(data)  def deep_update(origdict, newdict): -    """Update a dictionary in place, recursing to do a deep copy. +    """Update a dictionary in place, recursing to do a depth-first deep copy.      """      for key, value in newdict.items():          if isinstance(value, Mapping): diff --git a/lib/python/qmk/tests/minimal_info.json b/lib/python/qmk/tests/minimal_info.json index b91c23bd3d..11ef12fefe 100644 --- a/lib/python/qmk/tests/minimal_info.json +++ b/lib/python/qmk/tests/minimal_info.json @@ -1,8 +1,6 @@  {      "keyboard_name": "tester",      "maintainer": "qmk", -    "height": 5, -    "width": 15,      "layouts": {          "LAYOUT": {              "layout": [ diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index afdbc81429..b39fe5e46d 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py @@ -31,13 +31,13 @@ def check_returncode(result, expected=[0]):      assert result.returncode in expected -def test_cformat(): -    result = check_subcommand('cformat', '-n', 'quantum/matrix.c') +def test_format_c(): +    result = check_subcommand('format-c', '-n', 'quantum/matrix.c')      check_returncode(result) -def test_cformat_all(): -    result = check_subcommand('cformat', '-n', '-a') +def test_format_c_all(): +    result = check_subcommand('format-c', '-n', '-a')      check_returncode(result, [0, 1]) @@ -80,8 +80,8 @@ def test_hello():      assert 'Hello,' in result.stdout -def test_pyformat(): -    result = check_subcommand('pyformat', '--dry-run') +def test_format_python(): +    result = check_subcommand('format-python', '--dry-run')      check_returncode(result)      assert 'Python code in `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout @@ -258,6 +258,12 @@ def test_generate_rules_mk():      assert 'MCU ?= atmega32u4' in result.stdout +def test_generate_version_h(): +    result = check_subcommand('generate-version-h') +    check_returncode(result) +    assert '#define QMK_VERSION' in result.stdout + +  def test_generate_layouts():      result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')      check_returncode(result) @@ -267,7 +273,7 @@ def test_generate_layouts():  def test_format_json_keyboard():      result = check_subcommand('format-json', '--format', 'keyboard', 'lib/python/qmk/tests/minimal_info.json')      check_returncode(result) -    assert result.stdout == '{\n    "keyboard_name": "tester",\n    "maintainer": "qmk",\n    "height": 5,\n    "width": 15,\n    "layouts": {\n        "LAYOUT": {\n            "layout": [\n                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n            ]\n        }\n    }\n}\n' +    assert result.stdout == '{\n    "keyboard_name": "tester",\n    "maintainer": "qmk",\n    "layouts": {\n        "LAYOUT": {\n            "layout": [\n                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n            ]\n        }\n    }\n}\n'  def test_format_json_keymap(): @@ -279,7 +285,7 @@ def test_format_json_keymap():  def test_format_json_keyboard_auto():      result = check_subcommand('format-json', '--format', 'auto', 'lib/python/qmk/tests/minimal_info.json')      check_returncode(result) -    assert result.stdout == '{\n    "keyboard_name": "tester",\n    "maintainer": "qmk",\n    "height": 5,\n    "width": 15,\n    "layouts": {\n        "LAYOUT": {\n            "layout": [\n                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n            ]\n        }\n    }\n}\n' +    assert result.stdout == '{\n    "keyboard_name": "tester",\n    "maintainer": "qmk",\n    "layouts": {\n        "LAYOUT": {\n            "layout": [\n                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }\n            ]\n        }\n    }\n}\n'  def test_format_json_keymap_auto(): | 
