diff options
Diffstat (limited to 'lib/python/qmk')
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/cli/console.py | 303 | ||||
| -rw-r--r-- | lib/python/qmk/cli/doctor/check.py | 1 | ||||
| -rw-r--r-- | lib/python/qmk/cli/flash.py | 14 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/format/python.py | 6 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/generate/compilation_database.py | 133 | ||||
| -rw-r--r-- | lib/python/qmk/cli/generate/dfu_header.py | 2 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/generate/rules_mk.py | 9 | ||||
| -rw-r--r-- | lib/python/qmk/cli/lint.py | 145 | ||||
| -rw-r--r-- | lib/python/qmk/cli/pytest.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/commands.py | 16 | ||||
| -rw-r--r-- | lib/python/qmk/constants.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/info.py | 5 | ||||
| -rw-r--r-- | lib/python/qmk/tests/minimal_info.json | 2 | ||||
| -rw-r--r-- | lib/python/qmk/tests/test_cli_commands.py | 12 | 
15 files changed, 271 insertions, 383 deletions
| diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index ea961315bd..edf351d628 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -36,7 +36,6 @@ subcommands = [      'qmk.cli.chibios.confmigrate',      'qmk.cli.clean',      'qmk.cli.compile', -    'qmk.cli.console',      'qmk.cli.docs',      'qmk.cli.doctor',      'qmk.cli.fileformat', @@ -46,6 +45,7 @@ subcommands = [      'qmk.cli.format.python',      'qmk.cli.format.text',      'qmk.cli.generate.api', +    'qmk.cli.generate.compilation_database',      'qmk.cli.generate.config_h',      'qmk.cli.generate.dfu_header',      'qmk.cli.generate.docs', diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py deleted file mode 100644 index 98c6bc0dc0..0000000000 --- a/lib/python/qmk/cli/console.py +++ /dev/null @@ -1,303 +0,0 @@ -"""Acquire debugging information from usb hid devices - -cli implementation of https://www.pjrc.com/teensy/hid_listen.html -""" -from pathlib import Path -from threading import Thread -from time import sleep, strftime - -import hid -import usb.core - -from milc import cli - -LOG_COLOR = { -    'next': 0, -    'colors': [ -        '{fg_blue}', -        '{fg_cyan}', -        '{fg_green}', -        '{fg_magenta}', -        '{fg_red}', -        '{fg_yellow}', -    ], -} - -KNOWN_BOOTLOADERS = { -    # VID  ,  PID -    ('03EB', '2FEF'): 'atmel-dfu: ATmega16U2', -    ('03EB', '2FF0'): 'atmel-dfu: ATmega32U2', -    ('03EB', '2FF3'): 'atmel-dfu: ATmega16U4', -    ('03EB', '2FF4'): 'atmel-dfu: ATmega32U4', -    ('03EB', '2FF9'): 'atmel-dfu: AT90USB64', -    ('03EB', '2FFA'): 'atmel-dfu: AT90USB162', -    ('03EB', '2FFB'): 'atmel-dfu: AT90USB128', -    ('03EB', '6124'): 'Microchip SAM-BA', -    ('0483', 'DF11'): 'stm32-dfu: STM32 BOOTLOADER', -    ('16C0', '05DC'): 'usbasploader: USBaspLoader', -    ('16C0', '05DF'): 'bootloadhid: HIDBoot', -    ('16C0', '0478'): 'halfkay: Teensy Halfkay', -    ('1B4F', '9203'): 'caterina: Pro Micro 3.3V', -    ('1B4F', '9205'): 'caterina: Pro Micro 5V', -    ('1B4F', '9207'): 'caterina: LilyPadUSB', -    ('1C11', 'B007'): 'kiibohd: Kiibohd DFU Bootloader', -    ('1EAF', '0003'): 'stm32duino: Maple 003', -    ('1FFB', '0101'): 'caterina: Polou A-Star 32U4 Bootloader', -    ('2341', '0036'): 'caterina: Arduino Leonardo', -    ('2341', '0037'): 'caterina: Arduino Micro', -    ('239A', '000C'): 'caterina: Adafruit Feather 32U4', -    ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v', -    ('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', -    ('03EB', '2067'): 'qmk-hid: HID Bootloader', -    ('03EB', '2045'): 'lufa-ms: LUFA Mass Storage Bootloader' -} - - -class MonitorDevice(object): -    def __init__(self, hid_device, numeric): -        self.hid_device = hid_device -        self.numeric = numeric -        self.device = hid.Device(path=hid_device['path']) -        self.current_line = '' - -        cli.log.info('Console Connected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', hid_device) - -    def read(self, size, encoding='ascii', timeout=1): -        """Read size bytes from the device. -        """ -        return self.device.read(size, timeout).decode(encoding) - -    def read_line(self): -        """Read from the device's console until we get a \n. -        """ -        while '\n' not in self.current_line: -            self.current_line += self.read(32).replace('\x00', '') - -        lines = self.current_line.split('\n', 1) -        self.current_line = lines[1] - -        return lines[0] - -    def run_forever(self): -        while True: -            try: -                message = {**self.hid_device, 'text': self.read_line()} -                identifier = (int2hex(message['vendor_id']), int2hex(message['product_id'])) if self.numeric else (message['manufacturer_string'], message['product_string']) -                message['identifier'] = ':'.join(identifier) -                message['ts'] = '{style_dim}{fg_green}%s{style_reset_all} ' % (strftime(cli.config.general.datetime_fmt),) if cli.args.timestamp else '' - -                cli.echo('%(ts)s%(color)s%(identifier)s:%(index)d{style_reset_all}: %(text)s' % message) - -            except hid.HIDException: -                break - - -class FindDevices(object): -    def __init__(self, vid, pid, index, numeric): -        self.vid = vid -        self.pid = pid -        self.index = index -        self.numeric = numeric - -    def run_forever(self): -        """Process messages from our queue in a loop. -        """ -        live_devices = {} -        live_bootloaders = {} - -        while True: -            try: -                for device in list(live_devices): -                    if not live_devices[device]['thread'].is_alive(): -                        cli.log.info('Console Disconnected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', live_devices[device]) -                        del live_devices[device] - -                for device in self.find_devices(): -                    if device['path'] not in live_devices: -                        device['color'] = LOG_COLOR['colors'][LOG_COLOR['next']] -                        LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors']) -                        live_devices[device['path']] = device - -                        try: -                            monitor = MonitorDevice(device, self.numeric) -                            device['thread'] = Thread(target=monitor.run_forever, daemon=True) - -                            device['thread'].start() -                        except Exception as e: -                            device['e'] = e -                            device['e_name'] = e.__class__.__name__ -                            cli.log.error("Could not connect to %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s:%(vendor_id)04X:%(product_id)04X:%(index)d): %(e_name)s: %(e)s", device) -                            if cli.config.general.verbose: -                                cli.log.exception(e) -                            del live_devices[device['path']] - -                if cli.args.bootloaders: -                    for device in self.find_bootloaders(): -                        if device.address in live_bootloaders: -                            live_bootloaders[device.address]._qmk_found = True -                        else: -                            name = KNOWN_BOOTLOADERS[(int2hex(device.idVendor), int2hex(device.idProduct))] -                            cli.log.info('Bootloader Connected: {style_bright}{fg_magenta}%s', name) -                            device._qmk_found = True -                            live_bootloaders[device.address] = device - -                    for device in list(live_bootloaders): -                        if live_bootloaders[device]._qmk_found: -                            live_bootloaders[device]._qmk_found = False -                        else: -                            name = KNOWN_BOOTLOADERS[(int2hex(live_bootloaders[device].idVendor), int2hex(live_bootloaders[device].idProduct))] -                            cli.log.info('Bootloader Disconnected: {style_bright}{fg_magenta}%s', name) -                            del live_bootloaders[device] - -                sleep(.1) - -            except KeyboardInterrupt: -                break - -    def is_bootloader(self, hid_device): -        """Returns true if the device in question matches a known bootloader vid/pid. -        """ -        return (int2hex(hid_device.idVendor), int2hex(hid_device.idProduct)) in KNOWN_BOOTLOADERS - -    def is_console_hid(self, hid_device): -        """Returns true when the usage page indicates it's a teensy-style console. -        """ -        return hid_device['usage_page'] == 0xFF31 and hid_device['usage'] == 0x0074 - -    def is_filtered_device(self, hid_device): -        """Returns True if the device should be included in the list of available consoles. -        """ -        return int2hex(hid_device['vendor_id']) == self.vid and int2hex(hid_device['product_id']) == self.pid - -    def find_devices_by_report(self, hid_devices): -        """Returns a list of available teensy-style consoles by doing a brute-force search. - -        Some versions of linux don't report usage and usage_page. In that case we fallback to reading the report (possibly inaccurately) ourselves. -        """ -        devices = [] - -        for device in hid_devices: -            path = device['path'].decode('utf-8') - -            if path.startswith('/dev/hidraw'): -                number = path[11:] -                report = Path(f'/sys/class/hidraw/hidraw{number}/device/report_descriptor') - -                if report.exists(): -                    rp = report.read_bytes() - -                    if rp[1] == 0x31 and rp[3] == 0x09: -                        devices.append(device) - -        return devices - -    def find_bootloaders(self): -        """Returns a list of available bootloader devices. -        """ -        return list(filter(self.is_bootloader, usb.core.find(find_all=True))) - -    def find_devices(self): -        """Returns a list of available teensy-style consoles. -        """ -        hid_devices = hid.enumerate() -        devices = list(filter(self.is_console_hid, hid_devices)) - -        if not devices: -            devices = self.find_devices_by_report(hid_devices) - -        if self.vid and self.pid: -            devices = list(filter(self.is_filtered_device, devices)) - -        # Add index numbers -        device_index = {} -        for device in devices: -            id = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id']))) - -            if id not in device_index: -                device_index[id] = 0 - -            device_index[id] += 1 -            device['index'] = device_index[id] - -        return devices - - -def int2hex(number): -    """Returns a string representation of the number as hex. -    """ -    return "%04X" % number - - -def list_devices(device_finder): -    """Show the user a nicely formatted list of devices. -    """ -    devices = device_finder.find_devices() - -    if devices: -        cli.log.info('Available devices:') -        for dev in devices: -            color = LOG_COLOR['colors'][LOG_COLOR['next']] -            LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors']) -            cli.log.info("\t%s%s:%s:%d{style_reset_all}\t%s %s", color, int2hex(dev['vendor_id']), int2hex(dev['product_id']), dev['index'], dev['manufacturer_string'], dev['product_string']) - -    if cli.args.bootloaders: -        bootloaders = device_finder.find_bootloaders() - -        if bootloaders: -            cli.log.info('Available Bootloaders:') - -            for dev in bootloaders: -                cli.log.info("\t%s:%s\t%s", int2hex(dev.idVendor), int2hex(dev.idProduct), KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))]) - - [email protected]('--bootloaders', arg_only=True, default=True, action='store_boolean', help='displaying bootloaders.') [email protected]('-d', '--device', help='Device to select - uses format <pid>:<vid>[:<index>].') [email protected]('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') [email protected]('-n', '--numeric', arg_only=True, action='store_true', help='Show VID/PID instead of names.') [email protected]('-t', '--timestamp', arg_only=True, action='store_true', help='Print the timestamp for received messages as well.') [email protected]('-w', '--wait', type=int, default=1, help="How many seconds to wait between checks (Default: 1)") [email protected]('Acquire debugging information from usb hid devices.', hidden=False if cli.config.user.developer else True) -def console(cli): -    """Acquire debugging information from usb hid devices -    """ -    vid = None -    pid = None -    index = 1 - -    if cli.config.console.device: -        device = cli.config.console.device.split(':') - -        if len(device) == 2: -            vid, pid = device - -        elif len(device) == 3: -            vid, pid, index = device - -            if not index.isdigit(): -                cli.log.error('Device index must be a number! Got "%s" instead.', index) -                exit(1) - -            index = int(index) - -            if index < 1: -                cli.log.error('Device index must be greater than 0! Got %s', index) -                exit(1) - -        else: -            cli.log.error('Invalid format for device, expected "<pid>:<vid>[:<index>]" but got "%s".', cli.config.console.device) -            cli.print_help() -            exit(1) - -        vid = vid.upper() -        pid = pid.upper() - -    device_finder = FindDevices(vid, pid, index, cli.args.numeric) - -    if cli.args.list: -        return list_devices(device_finder) - -    print('Looking for devices...', flush=True) -    device_finder.run_forever() diff --git a/lib/python/qmk/cli/doctor/check.py b/lib/python/qmk/cli/doctor/check.py index 0807f41518..2d691b64b0 100644 --- a/lib/python/qmk/cli/doctor/check.py +++ b/lib/python/qmk/cli/doctor/check.py @@ -26,7 +26,6 @@ ESSENTIAL_BINARIES = {      'arm-none-eabi-gcc': {          'version_arg': '-dumpversion'      }, -    'bin/qmk': {},  } diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py index c2d9e09c69..28e48a4101 100644 --- a/lib/python/qmk/cli/flash.py +++ b/lib/python/qmk/cli/flash.py @@ -18,17 +18,21 @@ def print_bootloader_help():      """Prints the available bootloaders listed in docs.qmk.fm.      """      cli.log.info('Here are the available bootloaders:') +    cli.echo('\tavrdude') +    cli.echo('\tbootloadhid')      cli.echo('\tdfu') +    cli.echo('\tdfu-util') +    cli.echo('\tmdloader') +    cli.echo('\tst-flash') +    cli.echo('\tst-link-cli') +    cli.log.info('Enhanced variants for split keyboards:') +    cli.echo('\tavrdude-split-left') +    cli.echo('\tavrdude-split-right')      cli.echo('\tdfu-ee')      cli.echo('\tdfu-split-left')      cli.echo('\tdfu-split-right') -    cli.echo('\tavrdude') -    cli.echo('\tBootloadHID') -    cli.echo('\tdfu-util')      cli.echo('\tdfu-util-split-left')      cli.echo('\tdfu-util-split-right') -    cli.echo('\tst-link-cli') -    cli.echo('\tst-flash')      cli.echo('For more info, visit https://docs.qmk.fm/#/flashing') diff --git a/lib/python/qmk/cli/format/python.py b/lib/python/qmk/cli/format/python.py index 00612f97ec..b32a726401 100755 --- a/lib/python/qmk/cli/format/python.py +++ b/lib/python/qmk/cli/format/python.py @@ -11,15 +11,15 @@ 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'] +    yapf_cmd = ['yapf', '-vv', '--recursive', edit, '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.') +        cli.log.info('Python code in `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!') +            cli.log.error('Python code in `lib/python` is incorrectly formatted!')          else:              cli.log.error('Error formatting python code!') diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py new file mode 100755 index 0000000000..602635270c --- /dev/null +++ b/lib/python/qmk/cli/generate/compilation_database.py @@ -0,0 +1,133 @@ +"""Creates a compilation database for the given keyboard build. +""" + +import json +import os +import re +import shlex +import shutil +from functools import lru_cache +from pathlib import Path +from typing import Dict, Iterator, List, Union + +from milc import cli, MILC + +from qmk.commands import create_make_command +from qmk.constants import QMK_FIRMWARE +from qmk.decorators import automagic_keyboard, automagic_keymap + + +@lru_cache(maxsize=10) +def system_libs(binary: str) -> List[Path]: +    """Find the system include directory that the given build tool uses. +    """ +    cli.log.debug("searching for system library directory for binary: %s", binary) +    bin_path = shutil.which(binary) + +    # Actually query xxxxxx-gcc to find its include paths. +    if binary.endswith("gcc") or binary.endswith("g++"): +        result = cli.run([binary, '-E', '-Wp,-v', '-'], capture_output=True, check=True, input='\n') +        paths = [] +        for line in result.stderr.splitlines(): +            if line.startswith(" "): +                paths.append(Path(line.strip()).resolve()) +        return paths + +    return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else [] + + +file_re = re.compile(r'printf "Compiling: ([^"]+)') +cmd_re = re.compile(r'LOG=\$\((.+?)&&') + + +def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]: +    """parse the output of `make -n <target>` + +    This function makes many assumptions about the format of your build log. +    This happens to work right now for qmk. +    """ + +    state = 'start' +    this_file = None +    records = [] +    for line in f: +        if state == 'start': +            m = file_re.search(line) +            if m: +                this_file = m.group(1) +                state = 'cmd' + +        if state == 'cmd': +            assert this_file +            m = cmd_re.search(line) +            if m: +                # we have a hit! +                this_cmd = m.group(1) +                args = shlex.split(this_cmd) +                for s in system_libs(args[0]): +                    args += ['-isystem', '%s' % s] +                new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork') +                records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file}) +                state = 'start' + +    return records + + [email protected]('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') [email protected]('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') [email protected]('Create a compilation database.') +@automagic_keyboard +@automagic_keymap +def generate_compilation_database(cli: MILC) -> Union[bool, int]: +    """Creates a compilation database for the given keyboard build. + +    Does a make clean, then a make -n for this target and uses the dry-run output to create +    a compilation database (compile_commands.json). This file can help some IDEs and +    IDE-like editors work better. For more information about this: + +        https://clang.llvm.org/docs/JSONCompilationDatabase.html +    """ +    command = None +    # check both config domains: the magic decorator fills in `generate_compilation_database` but the user is +    # more likely to have set `compile` in their config file. +    current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard +    current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap + +    if current_keyboard and current_keymap: +        # Generate the make command for a specific keyboard/keymap. +        command = create_make_command(current_keyboard, current_keymap, dry_run=True) +    elif not current_keyboard: +        cli.log.error('Could not determine keyboard!') +    elif not current_keymap: +        cli.log.error('Could not determine keymap!') + +    if not command: +        cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') +        cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]') +        return False + +    # remove any environment variable overrides which could trip us up +    env = os.environ.copy() +    env.pop("MAKEFLAGS", None) + +    # re-use same executable as the main make invocation (might be gmake) +    clean_command = [command[0], 'clean'] +    cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command)) +    cli.run(clean_command, capture_output=False, check=True, env=env) + +    cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command)) + +    result = cli.run(command, capture_output=True, check=True, env=env) +    db = parse_make_n(result.stdout.splitlines()) +    if not db: +        cli.log.error("Failed to parse output from make output:\n%s", result.stdout) +        return False + +    cli.log.info("Found %s compile commands", len(db)) + +    dbpath = QMK_FIRMWARE / 'compile_commands.json' + +    cli.log.info(f"Writing build database to {dbpath}") +    dbpath.write_text(json.dumps(db, indent=4)) + +    return True diff --git a/lib/python/qmk/cli/generate/dfu_header.py b/lib/python/qmk/cli/generate/dfu_header.py index 5a1b109f1e..7fb585fc7d 100644 --- a/lib/python/qmk/cli/generate/dfu_header.py +++ b/lib/python/qmk/cli/generate/dfu_header.py @@ -32,7 +32,7 @@ def generate_dfu_header(cli):      keyboard_h_lines = ['/* This file was generated by `qmk generate-dfu-header`. Do not edit or copy.', ' */', '', '#pragma once']      keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}') -    keyboard_h_lines.append(f'#define PRODUCT {cli.config.generate_dfu_header.keyboard} Bootloader') +    keyboard_h_lines.append(f'#define PRODUCT {kb_info_json["keyboard_name"]} Bootloader')      # Optional      if 'qmk_lufa_bootloader.esc_output' in kb_info_json: diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index dcaff29fae..5d8d7cc8a7 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -67,12 +67,9 @@ def generate_rules_mk(cli):      # Iterate through features to enable/disable them      if 'features' in kb_info_json:          for feature, enabled in kb_info_json['features'].items(): -            if feature == 'bootmagic_lite' and enabled: -                rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite') -            else: -                feature = feature.upper() -                enabled = 'yes' if enabled else 'no' -                rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') +            feature = feature.upper() +            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': diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py index 02b31fbc41..96593ed69b 100644 --- a/lib/python/qmk/cli/lint.py +++ b/lib/python/qmk/cli/lint.py @@ -1,72 +1,129 @@  """Command to look over a keyboard/keymap and check for common mistakes.  """ +from pathlib import Path +  from milc import cli  from qmk.decorators import automagic_keyboard, automagic_keymap  from qmk.info import info_json -from qmk.keyboard import find_readme, keyboard_completer +from qmk.keyboard import keyboard_completer, list_keyboards  from qmk.keymap import locate_keymap  from qmk.path import is_keyboard, keyboard [email protected]('--strict', action='store_true', help='Treat warnings as errors.') [email protected]('-kb', '--keyboard', completer=keyboard_completer, help='The keyboard to check.') [email protected]('-km', '--keymap', help='The keymap to check.') +def keymap_check(kb, km): +    """Perform the keymap level checks. +    """ +    ok = True +    keymap_path = locate_keymap(kb, km) + +    if not keymap_path: +        ok = False +        cli.log.error("%s: Can't find %s keymap.", kb, km) + +    return ok + + +def rules_mk_assignment_only(keyboard_path): +    """Check the keyboard-level rules.mk to ensure it only has assignments. +    """ +    current_path = Path() +    errors = [] + +    for path_part in keyboard_path.parts: +        current_path = current_path / path_part +        rules_mk = current_path / 'rules.mk' + +        if rules_mk.exists(): +            continuation = None + +            for i, line in enumerate(rules_mk.open()): +                line = line.strip() + +                if '#' in line: +                    line = line[:line.index('#')] + +                if continuation: +                    line = continuation + line +                    continuation = None + +                if line: +                    if line[-1] == '\\': +                        continuation = line[:-1] +                        continue + +                    if line and '=' not in line: +                        errors.append(f'Non-assignment code on line +{i} {rules_mk}: {line}') + +    return errors + + [email protected]('--strict', action='store_true', help='Treat warnings as errors') [email protected]('-kb', '--keyboard', completer=keyboard_completer, help='Comma separated list of keyboards to check') [email protected]('-km', '--keymap', help='The keymap to check') [email protected]('--all-kb', action='store_true', arg_only=True, help='Check all keyboards')  @cli.subcommand('Check keyboard and keymap for common mistakes.')  @automagic_keyboard  @automagic_keymap  def lint(cli):      """Check keyboard and keymap for common mistakes.      """ -    if not cli.config.lint.keyboard: -        cli.log.error('Missing required argument: --keyboard') -        cli.print_help() -        return False +    failed = [] -    if not is_keyboard(cli.config.lint.keyboard): -        cli.log.error('No such keyboard: %s', cli.config.lint.keyboard) -        return False +    # Determine our keyboard list +    if cli.args.all_kb: +        if cli.args.keyboard: +            cli.log.warning('Both --all-kb and --keyboard passed, --all-kb takes presidence.') -    # Gather data about the keyboard. -    ok = True -    keyboard_path = keyboard(cli.config.lint.keyboard) -    keyboard_info = info_json(cli.config.lint.keyboard) -    readme_path = find_readme(cli.config.lint.keyboard) -    missing_readme_path = keyboard_path / 'readme.md' +        keyboard_list = list_keyboards() +    elif not cli.config.lint.keyboard: +        cli.log.error('Missing required arguments: --keyboard or --all-kb') +        cli.print_help() +        return False +    else: +        keyboard_list = cli.config.lint.keyboard.split(',') -    # Check for errors in the info.json -    if keyboard_info['parse_errors']: -        ok = False -        cli.log.error('Errors found when generating info.json.') +    # Lint each keyboard +    for kb in keyboard_list: +        if not is_keyboard(kb): +            cli.log.error('No such keyboard: %s', kb) +            continue -    if cli.config.lint.strict and keyboard_info['parse_warnings']: -        ok = False -        cli.log.error('Warnings found when generating info.json (Strict mode enabled.)') +        # Gather data about the keyboard. +        ok = True +        keyboard_path = keyboard(kb) +        keyboard_info = info_json(kb) -    # Check for a readme.md and warn if it doesn't exist -    if not readme_path: -        ok = False -        cli.log.error('Missing %s', missing_readme_path) +        # Check for errors in the info.json +        if keyboard_info['parse_errors']: +            ok = False +            cli.log.error('%s: Errors found when generating info.json.', kb) -    # Keymap specific checks -    if cli.config.lint.keymap: -        keymap_path = locate_keymap(cli.config.lint.keyboard, cli.config.lint.keymap) +        if cli.config.lint.strict and keyboard_info['parse_warnings']: +            ok = False +            cli.log.error('%s: Warnings found when generating info.json (Strict mode enabled.)', kb) -        if not keymap_path: +        # Check the rules.mk file(s) +        rules_mk_assignment_errors = rules_mk_assignment_only(keyboard_path) +        if rules_mk_assignment_errors:              ok = False -            cli.log.error("Can't find %s keymap for %s keyboard.", cli.config.lint.keymap, cli.config.lint.keyboard) -        else: -            keymap_readme = keymap_path.parent / 'readme.md' -            if not keymap_readme.exists(): -                cli.log.warning('Missing %s', keymap_readme) +            cli.log.error('%s: Non-assignment code found in rules.mk. Move it to post_rules.mk instead.', kb) +            for assignment_error in rules_mk_assignment_errors: +                cli.log.error(assignment_error) -                if cli.config.lint.strict: -                    ok = False +        # Keymap specific checks +        if cli.config.lint.keymap: +            if not keymap_check(kb, cli.config.lint.keymap): +                ok = False + +        # Report status +        if not ok: +            failed.append(kb)      # Check and report the overall status -    if ok: -        cli.log.info('Lint check passed!') -        return True +    if failed: +        cli.log.error('Lint check failed for: %s', ', '.join(failed)) +        return False -    cli.log.error('Lint check failed!') -    return False +    cli.log.info('Lint check passed!') +    return True diff --git a/lib/python/qmk/cli/pytest.py b/lib/python/qmk/cli/pytest.py index bdb336b9a7..a7f01a872a 100644 --- a/lib/python/qmk/cli/pytest.py +++ b/lib/python/qmk/cli/pytest.py @@ -12,6 +12,6 @@ def pytest(cli):      """Run several linting/testing commands.      """      nose2 = cli.run(['nose2', '-v'], capture_output=False, stdin=DEVNULL) -    flake8 = cli.run(['flake8', 'lib/python', 'bin/qmk'], capture_output=False, stdin=DEVNULL) +    flake8 = cli.run(['flake8', 'lib/python'], capture_output=False, stdin=DEVNULL)      return flake8.returncode | nose2.returncode diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 421453d837..2995a5fdab 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -28,7 +28,7 @@ def _find_make():      return make_cmd -def create_make_target(target, parallel=1, **env_vars): +def create_make_target(target, dry_run=False, parallel=1, **env_vars):      """Create a make command      Args: @@ -36,6 +36,9 @@ def create_make_target(target, parallel=1, **env_vars):          target              Usually a make rule, such as 'clean' or 'all'. +        dry_run +            make -n -- don't actually build +          parallel              The number of make jobs to run in parallel @@ -52,10 +55,10 @@ def create_make_target(target, parallel=1, **env_vars):      for key, value in env_vars.items():          env.append(f'{key}={value}') -    return [make_cmd, *get_make_parallel_args(parallel), *env, target] +    return [make_cmd, *(['-n'] if dry_run else []), *get_make_parallel_args(parallel), *env, target] -def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars): +def create_make_command(keyboard, keymap, target=None, dry_run=False, parallel=1, **env_vars):      """Create a make compile command      Args: @@ -69,6 +72,9 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):          target              Usually a bootloader. +        dry_run +            make -n -- don't actually build +          parallel              The number of make jobs to run in parallel @@ -84,7 +90,7 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):      if target:          make_args.append(target) -    return create_make_target(':'.join(make_args), parallel, **env_vars) +    return create_make_target(':'.join(make_args), dry_run=dry_run, parallel=parallel, **env_vars)  def get_git_version(current_time, repo_dir='.', check_dir='.'): @@ -233,7 +239,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va          f'VERBOSE={verbose}',          f'COLOR={color}',          'SILENT=false', -        f'QMK_BIN={"bin/qmk" if "DEPRECATED_BIN_QMK" in os.environ else "qmk"}', +        'QMK_BIN="qmk"',      ])      return make_command diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 71a6c91c77..73f596ba2c 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -13,7 +13,7 @@ QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware'  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', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443' +CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66FX1M0', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L433', 'STM32L443', 'GD32VF103'  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 cc99484519..dc42fdd4d9 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -122,11 +122,6 @@ def _extract_features(info_data, rules):      if rules.get('BOOTMAGIC_ENABLE') == 'full':          rules['BOOTMAGIC_ENABLE'] = 'on' -    # Skip non-boolean features we haven't implemented special handling for -    for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE': -        if rules.get(feature): -            del rules[feature] -      # Process the rest of the rules as booleans      for key, value in rules.items():          if key.endswith('_ENABLE'): diff --git a/lib/python/qmk/tests/minimal_info.json b/lib/python/qmk/tests/minimal_info.json index 11ef12fefe..3aae4722bf 100644 --- a/lib/python/qmk/tests/minimal_info.json +++ b/lib/python/qmk/tests/minimal_info.json @@ -4,7 +4,7 @@      "layouts": {          "LAYOUT": {              "layout": [ -                { "label": "KC_A", "x": 0, "y": 0, "matrix": [0, 0] } +                { "label": "KC_A", "matrix": [0, 0], "x": 0, "y": 0 }              ]          }      } diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py index b39fe5e46d..9f2c258ab4 100644 --- a/lib/python/qmk/tests/test_cli_commands.py +++ b/lib/python/qmk/tests/test_cli_commands.py @@ -83,7 +83,7 @@ def test_hello():  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 +    assert 'Python code in `lib/python` is correctly formatted.' in result.stdout  def test_list_keyboards(): @@ -151,7 +151,7 @@ def test_json2c_stdin():  def test_info():      result = check_subcommand('info', '-kb', 'handwired/pytest/basic')      check_returncode(result) -    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout +    assert 'Keyboard Name: pytest' in result.stdout      assert 'Processor: atmega32u4' in result.stdout      assert 'Layout:' not in result.stdout      assert 'k0' not in result.stdout @@ -160,7 +160,7 @@ def test_info():  def test_info_keyboard_render():      result = check_subcommand('info', '-kb', 'handwired/pytest/basic', '-l')      check_returncode(result) -    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout +    assert 'Keyboard Name: pytest' in result.stdout      assert 'Processor: atmega32u4' in result.stdout      assert 'Layouts:' in result.stdout      assert 'k0' in result.stdout @@ -169,7 +169,7 @@ def test_info_keyboard_render():  def test_info_keymap_render():      result = check_subcommand('info', '-kb', 'handwired/pytest/basic', '-km', 'default_json')      check_returncode(result) -    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout +    assert 'Keyboard Name: pytest' in result.stdout      assert 'Processor: atmega32u4' in result.stdout      if is_windows: @@ -181,7 +181,7 @@ def test_info_keymap_render():  def test_info_matrix_render():      result = check_subcommand('info', '-kb', 'handwired/pytest/basic', '-m')      check_returncode(result) -    assert 'Keyboard Name: handwired/pytest/basic' in result.stdout +    assert 'Keyboard Name: pytest' in result.stdout      assert 'Processor: atmega32u4' in result.stdout      assert 'LAYOUT_ortho_1x1' in result.stdout @@ -242,7 +242,7 @@ def test_generate_config_h():      assert '#   define DESCRIPTION handwired/pytest/basic' in result.stdout      assert '#   define DIODE_DIRECTION COL2ROW' in result.stdout      assert '#   define MANUFACTURER none' in result.stdout -    assert '#   define PRODUCT handwired/pytest/basic' in result.stdout +    assert '#   define PRODUCT pytest' in result.stdout      assert '#   define PRODUCT_ID 0x6465' in result.stdout      assert '#   define VENDOR_ID 0xFEED' in result.stdout      assert '#   define MATRIX_COLS 1' in result.stdout | 
