Allow for qmk mass-compile all:<keymap> (#22116)

Co-authored-by: Joel Challis <git@zvecr.com>
This commit is contained in:
Nick Brassel 2023-09-29 06:48:20 +10:00 committed by GitHub
parent fb0c64a567
commit c5706ef791
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 198 additions and 127 deletions

View File

@ -9,8 +9,69 @@ from milc import cli
from qmk.constants import QMK_FIRMWARE from qmk.constants import QMK_FIRMWARE
from qmk.commands import _find_make, get_make_parallel_args from qmk.commands import _find_make, get_make_parallel_args
from qmk.keyboard import resolve_keyboard from qmk.search import search_keymap_targets, search_make_targets
from qmk.search import search_keymap_targets
def mass_compile_targets(targets, clean, dry_run, no_temp, parallel, env):
if len(targets) == 0:
return
make_cmd = _find_make()
builddir = Path(QMK_FIRMWARE) / '.build'
makefile = builddir / 'parallel_kb_builds.mk'
if dry_run:
cli.log.info('Compilation targets:')
for target in sorted(targets):
cli.log.info(f"{{fg_cyan}}qmk compile -kb {target[0]} -km {target[1]}{{fg_reset}}")
else:
if clean:
cli.run([make_cmd, 'clean'], capture_output=False, stdin=DEVNULL)
builddir.mkdir(parents=True, exist_ok=True)
with open(makefile, "w") as f:
for target in sorted(targets):
keyboard_name = target[0]
keymap_name = target[1]
keyboard_safe = keyboard_name.replace('/', '_')
build_log = f"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}"
failed_log = f"{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}.{keymap_name}"
# yapf: disable
f.write(
f"""\
all: {keyboard_safe}_{keymap_name}_binary
{keyboard_safe}_{keymap_name}_binary:
@rm -f "{build_log}" || true
@echo "Compiling QMK Firmware for target: '{keyboard_name}:{keymap_name}'..." >>"{build_log}"
+@$(MAKE) -C "{QMK_FIRMWARE}" -f "{QMK_FIRMWARE}/builddefs/build_keyboard.mk" KEYBOARD="{keyboard_name}" KEYMAP="{keymap_name}" COLOR=true SILENT=false {' '.join(env)} \\
>>"{build_log}" 2>&1 \\
|| cp "{build_log}" "{failed_log}"
@{{ grep '\[ERRORS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
|| {{ grep '\[WARNINGS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;33m[WARNINGS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
|| printf "Build %-64s \e[1;32m[OK]\e[0m\\n" "{keyboard_name}:{keymap_name}"
@rm -f "{build_log}" || true
"""# noqa
)
# yapf: enable
if no_temp:
# yapf: disable
f.write(
f"""\
@rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.elf" 2>/dev/null || true
@rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.map" 2>/dev/null || true
@rm -rf "{QMK_FIRMWARE}/.build/obj_{keyboard_safe}_{keymap_name}" || true
"""# noqa
)
# yapf: enable
f.write('\n')
cli.run([make_cmd, *get_make_parallel_args(parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
# Check for failures
failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')]
if len(failures) > 0:
return False
@cli.argument('builds', nargs='*', arg_only=True, help="List of builds in form <keyboard>:<keymap> to compile in parallel. Specifying this overrides all other target search options.") @cli.argument('builds', nargs='*', arg_only=True, help="List of builds in form <keyboard>:<keymap> to compile in parallel. Specifying this overrides all other target search options.")
@ -33,67 +94,9 @@ from qmk.search import search_keymap_targets
def mass_compile(cli): def mass_compile(cli):
"""Compile QMK Firmware against all keyboards. """Compile QMK Firmware against all keyboards.
""" """
make_cmd = _find_make()
if cli.args.clean:
cli.run([make_cmd, 'clean'], capture_output=False, stdin=DEVNULL)
builddir = Path(QMK_FIRMWARE) / '.build'
makefile = builddir / 'parallel_kb_builds.mk'
if len(cli.args.builds) > 0: if len(cli.args.builds) > 0:
targets = list(sorted(set([(resolve_keyboard(e[0]), e[1]) for e in [b.split(':') for b in cli.args.builds]]))) targets = search_make_targets(cli.args.builds, cli.args.filter)
else: else:
targets = search_keymap_targets(cli.args.keymap, cli.args.filter) targets = search_keymap_targets(cli.args.keymap, cli.args.filter)
if len(targets) == 0: return mass_compile_targets(targets, cli.args.clean, cli.args.dry_run, cli.args.no_temp, cli.args.parallel, cli.args.env)
return
if cli.args.dry_run:
cli.log.info('Compilation targets:')
for target in sorted(targets):
cli.log.info(f"{{fg_cyan}}qmk compile -kb {target[0]} -km {target[1]}{{fg_reset}}")
else:
builddir.mkdir(parents=True, exist_ok=True)
with open(makefile, "w") as f:
for target in sorted(targets):
keyboard_name = target[0]
keymap_name = target[1]
keyboard_safe = keyboard_name.replace('/', '_')
build_log = f"{QMK_FIRMWARE}/.build/build.log.{os.getpid()}.{keyboard_safe}.{keymap_name}"
failed_log = f"{QMK_FIRMWARE}/.build/failed.log.{os.getpid()}.{keyboard_safe}.{keymap_name}"
# yapf: disable
f.write(
f"""\
all: {keyboard_safe}_{keymap_name}_binary
{keyboard_safe}_{keymap_name}_binary:
@rm -f "{build_log}" || true
@echo "Compiling QMK Firmware for target: '{keyboard_name}:{keymap_name}'..." >>"{build_log}"
+@$(MAKE) -C "{QMK_FIRMWARE}" -f "{QMK_FIRMWARE}/builddefs/build_keyboard.mk" KEYBOARD="{keyboard_name}" KEYMAP="{keymap_name}" COLOR=true SILENT=false {' '.join(cli.args.env)} \\
>>"{build_log}" 2>&1 \\
|| cp "{build_log}" "{failed_log}"
@{{ grep '\[ERRORS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
|| {{ grep '\[WARNINGS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;33m[WARNINGS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\
|| printf "Build %-64s \e[1;32m[OK]\e[0m\\n" "{keyboard_name}:{keymap_name}"
@rm -f "{build_log}" || true
"""# noqa
)
# yapf: enable
if cli.args.no_temp:
# yapf: disable
f.write(
f"""\
@rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.elf" 2>/dev/null || true
@rm -rf "{QMK_FIRMWARE}/.build/{keyboard_safe}_{keymap_name}.map" 2>/dev/null || true
@rm -rf "{QMK_FIRMWARE}/.build/obj_{keyboard_safe}_{keymap_name}" || true
"""# noqa
)
# yapf: enable
f.write('\n')
cli.run([make_cmd, *get_make_parallel_args(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
# Check for failures
failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')]
if len(failures) > 0:
return False

View File

@ -1,6 +1,7 @@
"""Functions that help us work with keyboards. """Functions that help us work with keyboards.
""" """
from array import array from array import array
from functools import lru_cache
from math import ceil from math import ceil
from pathlib import Path from pathlib import Path
import os import os
@ -144,6 +145,7 @@ def list_keyboards(resolve_defaults=True):
return sorted(set(found)) return sorted(set(found))
@lru_cache(maxsize=None)
def resolve_keyboard(keyboard): def resolve_keyboard(keyboard):
cur_dir = Path('keyboards') cur_dir = Path('keyboards')
rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk') rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')

View File

@ -1,10 +1,12 @@
"""Functions for searching through QMK keyboards and keymaps. """Functions for searching through QMK keyboards and keymaps.
""" """
import contextlib import contextlib
import functools
import fnmatch import fnmatch
import logging import logging
import multiprocessing import multiprocessing
import re import re
from typing import List, Tuple
from dotty_dict import dotty from dotty_dict import dotty
from milc import cli from milc import cli
@ -31,95 +33,159 @@ def ignore_logging():
def _all_keymaps(keyboard): def _all_keymaps(keyboard):
"""Returns a list of tuples of (keyboard, keymap) for all keymaps for the given keyboard.
"""
with ignore_logging(): with ignore_logging():
return (keyboard, qmk.keymap.list_keymaps(keyboard)) keyboard = qmk.keyboard.resolve_keyboard(keyboard)
return [(keyboard, keymap) for keymap in qmk.keymap.list_keymaps(keyboard)]
def _keymap_exists(keyboard, keymap): def _keymap_exists(keyboard, keymap):
"""Returns the keyboard name if the keyboard+keymap combination exists, otherwise None.
"""
with ignore_logging(): with ignore_logging():
return keyboard if qmk.keymap.locate_keymap(keyboard, keymap) is not None else None return keyboard if qmk.keymap.locate_keymap(keyboard, keymap) is not None else None
def _load_keymap_info(keyboard, keymap): def _load_keymap_info(kb_km):
"""Returns a tuple of (keyboard, keymap, info.json) for the given keyboard/keymap combination.
"""
with ignore_logging(): with ignore_logging():
return (keyboard, keymap, keymap_json(keyboard, keymap)) return (kb_km[0], kb_km[1], keymap_json(kb_km[0], kb_km[1]))
def search_keymap_targets(keymap='default', filters=[], print_vals=[]): def expand_make_targets(targets: List[str]) -> List[Tuple[str, str]]:
targets = [] """Expand a list of make targets into a list of (keyboard, keymap) tuples.
with multiprocessing.Pool() as pool: Caters for 'all' in either keyboard or keymap, or both.
cli.log.info(f'Retrieving list of keyboards with keymap "{keymap}"...') """
target_list = [] split_targets = []
for target in targets:
split_target = target.split(':')
if len(split_target) != 2:
cli.log.error(f"Invalid build target: {target}")
return []
split_targets.append((split_target[0], split_target[1]))
return expand_keymap_targets(split_targets)
def _expand_keymap_target(keyboard: str, keymap: str, all_keyboards: List[str] = None) -> List[Tuple[str, str]]:
"""Expand a keyboard input and keymap input into a list of (keyboard, keymap) tuples.
Caters for 'all' in either keyboard or keymap, or both.
"""
if all_keyboards is None:
all_keyboards = qmk.keyboard.list_keyboards()
if keyboard == 'all':
with multiprocessing.Pool() as pool:
if keymap == 'all':
cli.log.info('Retrieving list of all keyboards and keymaps...')
targets = []
for kb in pool.imap_unordered(_all_keymaps, all_keyboards):
targets.extend(kb)
return targets
else:
cli.log.info(f'Retrieving list of keyboards with keymap "{keymap}"...')
keyboard_filter = functools.partial(_keymap_exists, keymap=keymap)
return [(kb, keymap) for kb in filter(lambda e: e is not None, pool.imap_unordered(keyboard_filter, all_keyboards))]
else:
if keymap == 'all': if keymap == 'all':
kb_to_kms = pool.map(_all_keymaps, qmk.keyboard.list_keyboards()) keyboard = qmk.keyboard.resolve_keyboard(keyboard)
for targets in kb_to_kms: cli.log.info(f'Retrieving list of keymaps for keyboard "{keyboard}"...')
keyboard = targets[0] return _all_keymaps(keyboard)
keymaps = targets[1]
target_list.extend([(keyboard, keymap) for keymap in keymaps])
else: 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()]))] return [(qmk.keyboard.resolve_keyboard(keyboard), keymap)]
if len(filters) == 0:
targets = [(kb, km, {}) for kb, km in 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)]
function_re = re.compile(r'^(?P<function>[a-zA-Z]+)\((?P<key>[a-zA-Z0-9_\.]+)(,\s*(?P<value>[^#]+))?\)$') def expand_keymap_targets(targets: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
equals_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*=\s*(?P<value>[^#]+)$') """Expand a list of (keyboard, keymap) tuples inclusive of 'all', into a list of explicit (keyboard, keymap) tuples.
"""
overall_targets = []
all_keyboards = qmk.keyboard.list_keyboards()
for target in targets:
overall_targets.extend(_expand_keymap_target(target[0], target[1], all_keyboards))
return list(sorted(set(overall_targets)))
for filter_expr in filters:
function_match = function_re.match(filter_expr)
equals_match = equals_re.match(filter_expr)
if function_match is not None: def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str] = [], print_vals: List[str] = []) -> List[Tuple[str, str, List[Tuple[str, str]]]]:
func_name = function_match.group('function').lower() """Filter a list of (keyboard, keymap) tuples based on the supplied filters.
key = function_match.group('key')
value = function_match.group('value')
if value is not None: Optionally includes the values of the queried info.json keys.
if func_name == 'length': """
valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and len(e[2].get(key)) == int(value), valid_keymaps) if len(filters) == 0 and len(print_vals) == 0:
elif func_name == 'contains': targets = [(kb, km, {}) for kb, km in target_list]
valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and value in e[2].get(key), valid_keymaps) else:
else: cli.log.info('Parsing data for all matching keyboard/keymap combinations...')
cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}') with multiprocessing.Pool() as pool:
continue valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in pool.imap_unordered(_load_keymap_info, target_list)]
cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}, {{fg_cyan}}{value}{{fg_reset}})...') function_re = re.compile(r'^(?P<function>[a-zA-Z]+)\((?P<key>[a-zA-Z0-9_\.]+)(,\s*(?P<value>[^#]+))?\)$')
equals_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*=\s*(?P<value>[^#]+)$')
for filter_expr in filters:
function_match = function_re.match(filter_expr)
equals_match = equals_re.match(filter_expr)
if function_match is not None:
func_name = function_match.group('function').lower()
key = function_match.group('key')
value = function_match.group('value')
if value is not None:
if func_name == 'length':
valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and len(e[2].get(key)) == int(value), valid_keymaps)
elif func_name == 'contains':
valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and value in e[2].get(key), valid_keymaps)
else: else:
if func_name == 'exists': cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
valid_keymaps = filter(lambda e, key=key: key in e[2], valid_keymaps) continue
elif func_name == 'absent':
valid_keymaps = filter(lambda e, key=key: key not in e[2], valid_keymaps)
else:
cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
continue
cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}})...') cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}, {{fg_cyan}}{value}{{fg_reset}})...')
elif equals_match is not None:
key = equals_match.group('key')
value = equals_match.group('value')
cli.log.info(f'Filtering on condition: {{fg_cyan}}{key}{{fg_reset}} == {{fg_cyan}}{value}{{fg_reset}}...')
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)
else: else:
cli.log.warning(f'Unrecognized filter expression: {filter_expr}') if func_name == 'exists':
continue valid_keymaps = filter(lambda e, key=key: key in e[2], valid_keymaps)
elif func_name == 'absent':
valid_keymaps = filter(lambda e, key=key: key not in e[2], valid_keymaps)
else:
cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
continue
cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}})...')
elif equals_match is not None:
key = equals_match.group('key')
value = equals_match.group('value')
cli.log.info(f'Filtering on condition: {{fg_cyan}}{key}{{fg_reset}} == {{fg_cyan}}{value}{{fg_reset}}...')
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)
else:
cli.log.warning(f'Unrecognized filter expression: {filter_expr}')
continue
targets = [(e[0], e[1], [(p, e[2].get(p)) for p in print_vals]) for e in valid_keymaps] targets = [(e[0], e[1], [(p, e[2].get(p)) for p in print_vals]) for e in valid_keymaps]
return targets return targets
def search_keymap_targets(keymap='default', filters: List[str] = [], print_vals: List[str] = []) -> List[Tuple[str, str, List[Tuple[str, str]]]]:
"""Search for build targets matching the supplied criteria.
"""
return list(sorted(_filter_keymap_targets(expand_keymap_targets([('all', keymap)]), filters, print_vals), key=lambda e: (e[0], e[1])))
def search_make_targets(targets: List[str], filters: List[str] = [], print_vals: List[str] = []) -> List[Tuple[str, str, List[Tuple[str, str]]]]:
"""Search for build targets matching the supplied criteria.
"""
return list(sorted(_filter_keymap_targets(expand_make_targets(targets), filters, print_vals), key=lambda e: (e[0], e[1])))