From 1b3f7fcf7d3d441623e065c0be7821c5c5d87c75 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Fri, 17 Mar 2023 07:35:49 +1100 Subject: [PATCH] Add `qmk find` command, reuse logic for `qmk mass-compile`. (#20139) --- docs/cli_commands.md | 43 ++++++++++--- lib/python/qmk/cli/__init__.py | 1 + lib/python/qmk/cli/find.py | 23 +++++++ lib/python/qmk/cli/mass_compile.py | 91 +-------------------------- lib/python/qmk/search.py | 99 ++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 96 deletions(-) create mode 100644 lib/python/qmk/cli/find.py create mode 100644 lib/python/qmk/search.py 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] qmk compile [-c] [-e =] [-j ] -kb -km ``` -**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 ` ``` @@ -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 ` @@ -556,7 +585,7 @@ qmk kle2json [-f] **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[a-zA-Z0-9_\.]+)\s*=\s*(?P[^#]+)$') - exists_re = re.compile(r'^exists\((?P[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[a-zA-Z0-9_\.]+)\s*=\s*(?P[^#]+)$') + exists_re = re.compile(r'^exists\((?P[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