Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>`
```
Expand All @@ -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
...
Expand All @@ -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.
```
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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**:
Expand Down Expand Up @@ -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.
Expand All @@ -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>`

Expand Down Expand Up @@ -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.
```

Expand Down
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
'qmk.cli.compile',
'qmk.cli.docs',
'qmk.cli.doctor',
'qmk.cli.find',
'qmk.cli.flash',
'qmk.cli.format.c',
'qmk.cli.format.json',
Expand Down
23 changes: 23 additions & 0 deletions lib/python/qmk/cli/find.py
Original file line number Diff line number Diff line change
@@ -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]}')
91 changes: 2 additions & 89 deletions lib/python/qmk/cli/mass_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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

Expand Down
99 changes: 99 additions & 0 deletions lib/python/qmk/search.py
Original file line number Diff line number Diff line change
@@ -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