diff options
| -rw-r--r-- | docs/cli_commands.md | 43 | ||||
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
| -rw-r--r-- | lib/python/qmk/cli/find.py | 23 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/mass_compile.py | 91 | ||||
| -rw-r--r-- | lib/python/qmk/search.py | 99 | 
5 files changed, 161 insertions, 96 deletions
| diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 019447075b..d759c9c35a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -20,7 +20,7 @@ qmk compile [-c] <configuratorExport.json>  qmk compile [-c] [-e <var>=<value>] [-j <num_jobs>] -kb <keyboard_name> -km <keymap_name>  ``` -**Usage in Keyboard Directory**:   +**Usage in Keyboard Directory**:  Must be in keyboard directory with a default keymap, or in keymap directory for keyboard, or supply one with `--keymap <keymap_name>`  ``` @@ -44,7 +44,7 @@ $ qmk compile  or with optional keymap argument  ``` -$ cd ~/qmk_firmware/keyboards/clueboard/66/rev4  +$ cd ~/qmk_firmware/keyboards/clueboard/66/rev4  $ qmk compile -km 66_iso  Ψ Compiling keymap with make clueboard/66/rev4:66_iso  ... @@ -58,7 +58,7 @@ $ qmk compile  ...  ``` -**Usage in Layout Directory**:   +**Usage in Layout Directory**:  Must be under `qmk_firmware/layouts/`, and in a keymap folder.  ``` @@ -149,6 +149,34 @@ To exit out into the parent shell, simply type `exit`.  qmk cd  ``` +## `qmk find` + +This command allows for searching through keyboard/keymap targets, filtering by specific criteria. `info.json` and `rules.mk` files contribute to the search data, as well as keymap configurations, and the results can be filtered using "dotty" syntax matching the overall `info.json` file format. + +For example, one could search for all keyboards using STM32F411: + +``` +qmk find -f 'processor=STM32F411' +``` + +...and one can further constrain the list to keyboards using STM32F411 as well as rgb_matrix support: + +``` +qmk find -f 'processor=STM32F411' -f 'features.rgb_matrix=true' +``` + +**Usage**: + +``` +qmk find [-h] [-km KEYMAP] [-f FILTER] + +options: +  -km KEYMAP, --keymap KEYMAP +                        The keymap name to build. Default is 'default'. +  -f FILTER, --filter FILTER +                        Filter the list of keyboards based on the supplied value in rules.mk. Matches info.json structure, and accepts the formats 'features.rgblight=true' or 'exists(matrix_pins.direct)'. May be passed multiple times, all filters need to match. Value may include wildcards such as '*' and '?'. +``` +  ## `qmk console`  This command lets you connect to keyboard consoles to get debugging messages. It only works if your keyboard firmware has been compiled with `CONSOLE_ENABLE=yes`. @@ -269,7 +297,8 @@ qmk json2c [-o OUTPUT] filename  ## `qmk c2json` -Creates a keymap.json from a keymap.c.   +Creates a keymap.json from a keymap.c. +  **Note:** Parsing C source files is not easy, therefore this subcommand may not work with your keymap. In some cases not using the C pre-processor helps.  **Usage**: @@ -442,7 +471,7 @@ $ qmk import-kbfirmware ~/Downloads/gh62.json  ## `qmk format-text` -This command formats text files to have proper line endings.  +This command formats text files to have proper line endings.  Every text file in the repository needs to have Unix (LF) line ending.  If you are working on **Windows**, you must ensure that line endings are corrected in order to get your PRs merged. @@ -453,7 +482,7 @@ qmk format-text  ## `qmk format-c` -This command formats C code using clang-format.  +This command formats C code using clang-format.  Run it with no arguments to format all core code that has been changed. Default checks `origin/master` with `git diff`, branch can be changed using `-b <branch_name>` @@ -556,7 +585,7 @@ qmk kle2json [-f] <filename>  **Examples**:  ``` -$ qmk kle2json kle.txt  +$ qmk kle2json kle.txt  ☒ File info.json already exists, use -f or --force to overwrite.  ``` diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 778eccada8..de7b0476a0 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -39,6 +39,7 @@ subcommands = [      'qmk.cli.compile',      'qmk.cli.docs',      'qmk.cli.doctor', +    'qmk.cli.find',      'qmk.cli.flash',      'qmk.cli.format.c',      'qmk.cli.format.json', diff --git a/lib/python/qmk/cli/find.py b/lib/python/qmk/cli/find.py new file mode 100644 index 0000000000..b6f74380ab --- /dev/null +++ b/lib/python/qmk/cli/find.py @@ -0,0 +1,23 @@ +"""Command to search through all keyboards and keymaps for a given search criteria. +""" +from milc import cli +from qmk.search import search_keymap_targets + + +@cli.argument( +    '-f', +    '--filter', +    arg_only=True, +    action='append', +    default=[], +    help=  # noqa: `format-python` and `pytest` don't agree here. +    "Filter the list of keyboards based on the supplied value in rules.mk. Matches info.json structure, and accepts the formats 'features.rgblight=true' or 'exists(matrix_pins.direct)'. May be passed multiple times, all filters need to match. Value may include wildcards such as '*' and '?'."  # noqa: `format-python` and `pytest` don't agree here. +) +@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.") +@cli.subcommand('Find builds which match supplied search criteria.') +def find(cli): +    """Search through all keyboards and keymaps for a given search criteria. +    """ +    targets = search_keymap_targets(cli.args.keymap, cli.args.filter) +    for target in targets: +        print(f'{target[0]}:{target[1]}') diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py index 810350b954..941e6aa411 100755 --- a/lib/python/qmk/cli/mass_compile.py +++ b/lib/python/qmk/cli/mass_compile.py @@ -2,52 +2,14 @@  This will compile everything in parallel, for testing purposes.  """ -import fnmatch -import logging -import multiprocessing  import os -import re  from pathlib import Path  from subprocess import DEVNULL -from dotty_dict import dotty  from milc import cli  from qmk.constants import QMK_FIRMWARE  from qmk.commands import _find_make, get_make_parallel_args -from qmk.info import keymap_json -import qmk.keyboard -import qmk.keymap - - -def _set_log_level(level): -    cli.acquire_lock() -    old = cli.log_level -    cli.log_level = level -    cli.log.setLevel(level) -    logging.root.setLevel(level) -    cli.release_lock() -    return old - - -def _all_keymaps(keyboard): -    old = _set_log_level(logging.CRITICAL) -    keymaps = qmk.keymap.list_keymaps(keyboard) -    _set_log_level(old) -    return (keyboard, keymaps) - - -def _keymap_exists(keyboard, keymap): -    old = _set_log_level(logging.CRITICAL) -    ret = keyboard if qmk.keymap.locate_keymap(keyboard, keymap) is not None else None -    _set_log_level(old) -    return ret - - -def _load_keymap_info(keyboard, keymap): -    old = _set_log_level(logging.CRITICAL) -    ret = (keyboard, keymap, keymap_json(keyboard, keymap)) -    _set_log_level(old) -    return ret +from qmk.search import search_keymap_targets  @cli.argument('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.") @@ -75,56 +37,7 @@ def mass_compile(cli):      builddir = Path(QMK_FIRMWARE) / '.build'      makefile = builddir / 'parallel_kb_builds.mk' -    targets = [] - -    with multiprocessing.Pool() as pool: -        cli.log.info(f'Retrieving list of keyboards with keymap "{cli.args.keymap}"...') -        target_list = [] -        if cli.args.keymap == 'all': -            kb_to_kms = pool.map(_all_keymaps, qmk.keyboard.list_keyboards()) -            for targets in kb_to_kms: -                keyboard = targets[0] -                keymaps = targets[1] -                target_list.extend([(keyboard, keymap) for keymap in keymaps]) -        else: -            target_list = [(kb, cli.args.keymap) for kb in filter(lambda kb: kb is not None, pool.starmap(_keymap_exists, [(kb, cli.args.keymap) for kb in qmk.keyboard.list_keyboards()]))] - -        if len(cli.args.filter) == 0: -            targets = target_list -        else: -            cli.log.info('Parsing data for all matching keyboard/keymap combinations...') -            valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in pool.starmap(_load_keymap_info, target_list)] - -            equals_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*=\s*(?P<value>[^#]+)$') -            exists_re = re.compile(r'^exists\((?P<key>[a-zA-Z0-9_\.]+)\)$') -            for filter_txt in cli.args.filter: -                f = equals_re.match(filter_txt) -                if f is not None: -                    key = f.group('key') -                    value = f.group('value') -                    cli.log.info(f'Filtering on condition ("{key}" == "{value}")...') - -                    def _make_filter(k, v): -                        expr = fnmatch.translate(v) -                        rule = re.compile(f'^{expr}$', re.IGNORECASE) - -                        def f(e): -                            lhs = e[2].get(k) -                            lhs = str(False if lhs is None else lhs) -                            return rule.search(lhs) is not None - -                        return f - -                    valid_keymaps = filter(_make_filter(key, value), valid_keymaps) - -                f = exists_re.match(filter_txt) -                if f is not None: -                    key = f.group('key') -                    cli.log.info(f'Filtering on condition (exists: "{key}")...') -                    valid_keymaps = filter(lambda e: e[2].get(key) is not None, valid_keymaps) - -            targets = [(e[0], e[1]) for e in valid_keymaps] - +    targets = search_keymap_targets(cli.args.keymap, cli.args.filter)      if len(targets) == 0:          return diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py new file mode 100644 index 0000000000..af48900e6b --- /dev/null +++ b/lib/python/qmk/search.py @@ -0,0 +1,99 @@ +"""Functions for searching through QMK keyboards and keymaps. +""" +import contextlib +import fnmatch +import logging +import multiprocessing +import re +from dotty_dict import dotty +from milc import cli + +from qmk.info import keymap_json +import qmk.keyboard +import qmk.keymap + + +def _set_log_level(level): +    cli.acquire_lock() +    old = cli.log_level +    cli.log_level = level +    cli.log.setLevel(level) +    logging.root.setLevel(level) +    cli.release_lock() +    return old + + +@contextlib.contextmanager +def ignore_logging(): +    old = _set_log_level(logging.CRITICAL) +    yield +    _set_log_level(old) + + +def _all_keymaps(keyboard): +    with ignore_logging(): +        return (keyboard, qmk.keymap.list_keymaps(keyboard)) + + +def _keymap_exists(keyboard, keymap): +    with ignore_logging(): +        return keyboard if qmk.keymap.locate_keymap(keyboard, keymap) is not None else None + + +def _load_keymap_info(keyboard, keymap): +    with ignore_logging(): +        return (keyboard, keymap, keymap_json(keyboard, keymap)) + + +def search_keymap_targets(keymap='default', filters=[]): +    targets = [] + +    with multiprocessing.Pool() as pool: +        cli.log.info(f'Retrieving list of keyboards with keymap "{keymap}"...') +        target_list = [] +        if keymap == 'all': +            kb_to_kms = pool.map(_all_keymaps, qmk.keyboard.list_keyboards()) +            for targets in kb_to_kms: +                keyboard = targets[0] +                keymaps = targets[1] +                target_list.extend([(keyboard, keymap) for keymap in keymaps]) +        else: +            target_list = [(kb, keymap) for kb in filter(lambda kb: kb is not None, pool.starmap(_keymap_exists, [(kb, keymap) for kb in qmk.keyboard.list_keyboards()]))] + +        if len(filters) == 0: +            targets = target_list +        else: +            cli.log.info('Parsing data for all matching keyboard/keymap combinations...') +            valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in pool.starmap(_load_keymap_info, target_list)] + +            equals_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*=\s*(?P<value>[^#]+)$') +            exists_re = re.compile(r'^exists\((?P<key>[a-zA-Z0-9_\.]+)\)$') +            for filter_txt in filters: +                f = equals_re.match(filter_txt) +                if f is not None: +                    key = f.group('key') +                    value = f.group('value') +                    cli.log.info(f'Filtering on condition ("{key}" == "{value}")...') + +                    def _make_filter(k, v): +                        expr = fnmatch.translate(v) +                        rule = re.compile(f'^{expr}$', re.IGNORECASE) + +                        def f(e): +                            lhs = e[2].get(k) +                            lhs = str(False if lhs is None else lhs) +                            return rule.search(lhs) is not None + +                        return f + +                    valid_keymaps = filter(_make_filter(key, value), valid_keymaps) + +                f = exists_re.match(filter_txt) +                if f is not None: +                    key = f.group('key') +                    cli.log.info(f'Filtering on condition (exists: "{key}")...') +                    valid_keymaps = filter(lambda e: e[2].get(key) is not None, valid_keymaps) + +            targets = [(e[0], e[1]) for e in valid_keymaps] + +    return targets | 
