diff options
Diffstat (limited to 'lib/python')
| -rwxr-xr-x | lib/python/qmk/cli/mass_compile.py | 77 | ||||
| -rw-r--r-- | lib/python/qmk/keyboard.py | 2 | ||||
| -rw-r--r-- | lib/python/qmk/search.py | 204 | 
3 files changed, 177 insertions, 106 deletions
| diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py index 1032dc82d1..1227f435e7 100755 --- a/lib/python/qmk/cli/mass_compile.py +++ b/lib/python/qmk/cli/mass_compile.py @@ -9,50 +9,25 @@ from milc import cli  from qmk.constants import QMK_FIRMWARE  from qmk.commands import _find_make, get_make_parallel_args -from qmk.keyboard import resolve_keyboard -from qmk.search import search_keymap_targets +from qmk.search import search_keymap_targets, search_make_targets [email protected]('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.") [email protected]('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.") [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.") [email protected]('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") [email protected]('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the commands to be run.") -    '-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. -) [email protected]('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.") [email protected]('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") [email protected]('Compile QMK Firmware for all keyboards.', hidden=False if cli.config.user.developer else True) -def mass_compile(cli): -    """Compile QMK Firmware against all keyboards. -    """ -    make_cmd = _find_make() -    if cli.args.clean: -        cli.run([make_cmd, 'clean'], capture_output=False, stdin=DEVNULL) +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 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]]))) -    else: -        targets = search_keymap_targets(cli.args.keymap, cli.args.filter) - -    if len(targets) == 0: -        return - -    if cli.args.dry_run: +    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): @@ -68,7 +43,7 @@ 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)} \\ +	+@$(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}" ; }} \\ @@ -79,7 +54,7 @@ all: {keyboard_safe}_{keymap_name}_binary                  )                  # yapf: enable -                if cli.args.no_temp: +                if no_temp:                      # yapf: disable                      f.write(                          f"""\ @@ -91,9 +66,37 @@ all: {keyboard_safe}_{keymap_name}_binary                      # 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) +        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 + + [email protected]('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.") [email protected]('-t', '--no-temp', arg_only=True, action='store_true', help="Remove temporary files during build.") [email protected]('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.") [email protected]('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.") [email protected]('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the commands to be run.") +    '-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. +) [email protected]('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.") [email protected]('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.") [email protected]('Compile QMK Firmware for all keyboards.', hidden=False if cli.config.user.developer else True) +def mass_compile(cli): +    """Compile QMK Firmware against all keyboards. +    """ +    if len(cli.args.builds) > 0: +        targets = search_make_targets(cli.args.builds, cli.args.filter) +    else: +        targets = search_keymap_targets(cli.args.keymap, cli.args.filter) + +    return mass_compile_targets(targets, cli.args.clean, cli.args.dry_run, cli.args.no_temp, cli.args.parallel, cli.args.env) diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py index 3e5cae4b22..18ca5a9534 100644 --- a/lib/python/qmk/keyboard.py +++ b/lib/python/qmk/keyboard.py @@ -1,6 +1,7 @@  """Functions that help us work with keyboards.  """  from array import array +from functools import lru_cache  from math import ceil  from pathlib import Path  import os @@ -144,6 +145,7 @@ def list_keyboards(resolve_defaults=True):      return sorted(set(found)) +@lru_cache(maxsize=None)  def resolve_keyboard(keyboard):      cur_dir = Path('keyboards')      rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk') diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py index 2bbbc7806f..0b5d489218 100644 --- a/lib/python/qmk/search.py +++ b/lib/python/qmk/search.py @@ -1,10 +1,12 @@  """Functions for searching through QMK keyboards and keymaps.  """  import contextlib +import functools  import fnmatch  import logging  import multiprocessing  import re +from typing import List, Tuple  from dotty_dict import dotty  from milc import cli @@ -31,95 +33,159 @@ def ignore_logging():  def _all_keymaps(keyboard): +    """Returns a list of tuples of (keyboard, keymap) for all keymaps for the given keyboard. +    """      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): +    """Returns the keyboard name if the keyboard+keymap combination exists, otherwise None. +    """      with ignore_logging():          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(): -        return (keyboard, keymap, keymap_json(keyboard, keymap)) - - -def search_keymap_targets(keymap='default', filters=[], print_vals=[]): -    targets = [] - -    with multiprocessing.Pool() as pool: -        cli.log.info(f'Retrieving list of keyboards with keymap "{keymap}"...') -        target_list = [] +        return (kb_km[0], kb_km[1], keymap_json(kb_km[0], kb_km[1])) + + +def expand_make_targets(targets: List[str]) -> List[Tuple[str, str]]: +    """Expand a list of make targets into a list of (keyboard, keymap) tuples. + +    Caters for 'all' in either keyboard or keymap, or both. +    """ +    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': -            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]) +            keyboard = qmk.keyboard.resolve_keyboard(keyboard) +            cli.log.info(f'Retrieving list of keymaps for keyboard "{keyboard}"...') +            return _all_keymaps(keyboard)          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)] + + +def expand_keymap_targets(targets: List[Tuple[str, str]]) -> List[Tuple[str, str]]: +    """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))) + + +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]]]]: +    """Filter a list of (keyboard, keymap) tuples based on the supplied filters. + +    Optionally includes the values of the queried info.json keys. +    """ +    if len(filters) == 0 and len(print_vals) == 0: +        targets = [(kb, km, {}) for kb, km in target_list] +    else: +        cli.log.info('Parsing data for all matching keyboard/keymap combinations...') +        with multiprocessing.Pool() as pool: +            valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in pool.imap_unordered(_load_keymap_info, target_list)] + +        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: +                        cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}') +                        continue -        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>[^#]+))?\)$') -            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: -                            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}}, {{fg_cyan}}{value}{{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}})...') +                else: +                    if func_name == 'exists': +                        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: -                        if func_name == 'exists': -                            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.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}})...') -                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}}...') +            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 _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 +                    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 +                    return f -                    valid_keymaps = filter(_make_filter(key, value), valid_keymaps) -                else: -                    cli.log.warning(f'Unrecognized filter expression: {filter_expr}') -                    continue +                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]      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]))) | 
