Add `qmk find` command, reuse logic for `qmk mass-compile`. (#20139)

This commit is contained in:
Nick Brassel 2023-03-17 07:35:49 +11:00 committed by GitHub
parent f3f634ef67
commit 1b3f7fcf7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 96 deletions

View File

@ -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.
```

View File

@ -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',

View File

@ -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]}')

View File

@ -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

99
lib/python/qmk/search.py Normal file
View File

@ -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