diff options
Diffstat (limited to 'lib/python/qmk/cli/generate')
| -rwxr-xr-x | lib/python/qmk/cli/generate/compilation_database.py | 133 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/generate/develop_pr_list.py | 119 | ||||
| -rw-r--r-- | lib/python/qmk/cli/generate/dfu_header.py | 2 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/generate/rules_mk.py | 9 | 
4 files changed, 256 insertions, 7 deletions
| diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py new file mode 100755 index 0000000000..602635270c --- /dev/null +++ b/lib/python/qmk/cli/generate/compilation_database.py @@ -0,0 +1,133 @@ +"""Creates a compilation database for the given keyboard build. +""" + +import json +import os +import re +import shlex +import shutil +from functools import lru_cache +from pathlib import Path +from typing import Dict, Iterator, List, Union + +from milc import cli, MILC + +from qmk.commands import create_make_command +from qmk.constants import QMK_FIRMWARE +from qmk.decorators import automagic_keyboard, automagic_keymap + + +@lru_cache(maxsize=10) +def system_libs(binary: str) -> List[Path]: +    """Find the system include directory that the given build tool uses. +    """ +    cli.log.debug("searching for system library directory for binary: %s", binary) +    bin_path = shutil.which(binary) + +    # Actually query xxxxxx-gcc to find its include paths. +    if binary.endswith("gcc") or binary.endswith("g++"): +        result = cli.run([binary, '-E', '-Wp,-v', '-'], capture_output=True, check=True, input='\n') +        paths = [] +        for line in result.stderr.splitlines(): +            if line.startswith(" "): +                paths.append(Path(line.strip()).resolve()) +        return paths + +    return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else [] + + +file_re = re.compile(r'printf "Compiling: ([^"]+)') +cmd_re = re.compile(r'LOG=\$\((.+?)&&') + + +def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]: +    """parse the output of `make -n <target>` + +    This function makes many assumptions about the format of your build log. +    This happens to work right now for qmk. +    """ + +    state = 'start' +    this_file = None +    records = [] +    for line in f: +        if state == 'start': +            m = file_re.search(line) +            if m: +                this_file = m.group(1) +                state = 'cmd' + +        if state == 'cmd': +            assert this_file +            m = cmd_re.search(line) +            if m: +                # we have a hit! +                this_cmd = m.group(1) +                args = shlex.split(this_cmd) +                for s in system_libs(args[0]): +                    args += ['-isystem', '%s' % s] +                new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork') +                records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file}) +                state = 'start' + +    return records + + [email protected]('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.') [email protected]('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.') [email protected]('Create a compilation database.') +@automagic_keyboard +@automagic_keymap +def generate_compilation_database(cli: MILC) -> Union[bool, int]: +    """Creates a compilation database for the given keyboard build. + +    Does a make clean, then a make -n for this target and uses the dry-run output to create +    a compilation database (compile_commands.json). This file can help some IDEs and +    IDE-like editors work better. For more information about this: + +        https://clang.llvm.org/docs/JSONCompilationDatabase.html +    """ +    command = None +    # check both config domains: the magic decorator fills in `generate_compilation_database` but the user is +    # more likely to have set `compile` in their config file. +    current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard +    current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap + +    if current_keyboard and current_keymap: +        # Generate the make command for a specific keyboard/keymap. +        command = create_make_command(current_keyboard, current_keymap, dry_run=True) +    elif not current_keyboard: +        cli.log.error('Could not determine keyboard!') +    elif not current_keymap: +        cli.log.error('Could not determine keymap!') + +    if not command: +        cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') +        cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]') +        return False + +    # remove any environment variable overrides which could trip us up +    env = os.environ.copy() +    env.pop("MAKEFLAGS", None) + +    # re-use same executable as the main make invocation (might be gmake) +    clean_command = [command[0], 'clean'] +    cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command)) +    cli.run(clean_command, capture_output=False, check=True, env=env) + +    cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command)) + +    result = cli.run(command, capture_output=True, check=True, env=env) +    db = parse_make_n(result.stdout.splitlines()) +    if not db: +        cli.log.error("Failed to parse output from make output:\n%s", result.stdout) +        return False + +    cli.log.info("Found %s compile commands", len(db)) + +    dbpath = QMK_FIRMWARE / 'compile_commands.json' + +    cli.log.info(f"Writing build database to {dbpath}") +    dbpath.write_text(json.dumps(db, indent=4)) + +    return True diff --git a/lib/python/qmk/cli/generate/develop_pr_list.py b/lib/python/qmk/cli/generate/develop_pr_list.py new file mode 100755 index 0000000000..de4eaa7d88 --- /dev/null +++ b/lib/python/qmk/cli/generate/develop_pr_list.py @@ -0,0 +1,119 @@ +"""Export the initial list of PRs associated with a `develop` merge to `master`. +""" +import os +import re +from pathlib import Path +from subprocess import DEVNULL + +from milc import cli + +cache_timeout = 7 * 86400 +fix_expr = re.compile(r'fix', flags=re.IGNORECASE) +clean1_expr = re.compile(r'\[(develop|keyboard|keymap|core|cli|bug|docs|feature)\]', flags=re.IGNORECASE) +clean2_expr = re.compile(r'^(develop|keyboard|keymap|core|cli|bug|docs|feature):', flags=re.IGNORECASE) + + +def _get_pr_info(cache, gh, pr_num): +    pull = cache.get(f'pull:{pr_num}') +    if pull is None: +        print(f'Retrieving info for PR #{pr_num}') +        pull = gh.pulls.get(owner='qmk', repo='qmk_firmware', pull_number=pr_num) +        cache.set(f'pull:{pr_num}', pull, cache_timeout) +    return pull + + +def _try_open_cache(cli): +    # These dependencies are manually handled because people complain. Fun. +    try: +        from sqlite_cache.sqlite_cache import SqliteCache +    except ImportError: +        return None + +    cache_loc = Path(cli.config_file).parent +    return SqliteCache(cache_loc) + + +def _get_github(): +    try: +        from ghapi.all import GhApi +    except ImportError: +        return None + +    return GhApi() + + [email protected]('-f', '--from-ref', default='0.11.0', help='Git revision/tag/reference/branch to begin search') [email protected]('-b', '--branch', default='upstream/develop', help='Git branch to iterate (default: "upstream/develop")') [email protected]('Creates the develop PR list.', hidden=False if cli.config.user.developer else True) +def generate_develop_pr_list(cli): +    """Retrieves information from GitHub regarding the list of PRs associated +    with a merge of `develop` branch into `master`. + +    Requires environment variable GITHUB_TOKEN to be set. +    """ + +    if 'GITHUB_TOKEN' not in os.environ or os.environ['GITHUB_TOKEN'] == '': +        cli.log.error('Environment variable "GITHUB_TOKEN" is not set.') +        return 1 + +    cache = _try_open_cache(cli) +    gh = _get_github() + +    git_args = ['git', 'rev-list', '--oneline', '--no-merges', '--reverse', f'{cli.args.from_ref}...{cli.args.branch}', '^upstream/master'] +    commit_list = cli.run(git_args, capture_output=True, stdin=DEVNULL) + +    if cache is None or gh is None: +        cli.log.error('Missing one or more dependent python packages: "ghapi", "python-sqlite-cache"') +        return 1 + +    pr_list_bugs = [] +    pr_list_dependencies = [] +    pr_list_core = [] +    pr_list_keyboards = [] +    pr_list_keyboard_fixes = [] +    pr_list_cli = [] +    pr_list_others = [] + +    def _categorise_commit(commit_info): +        def fix_or_normal(info, fixes_collection, normal_collection): +            if "bug" in info['pr_labels'] or fix_expr.search(info['title']): +                fixes_collection.append(info) +            else: +                normal_collection.append(info) + +        if "dependencies" in commit_info['pr_labels']: +            fix_or_normal(commit_info, pr_list_bugs, pr_list_dependencies) +        elif "core" in commit_info['pr_labels']: +            fix_or_normal(commit_info, pr_list_bugs, pr_list_core) +        elif "keyboard" in commit_info['pr_labels'] or "keymap" in commit_info['pr_labels'] or "via" in commit_info['pr_labels']: +            fix_or_normal(commit_info, pr_list_keyboard_fixes, pr_list_keyboards) +        elif "cli" in commit_info['pr_labels']: +            fix_or_normal(commit_info, pr_list_bugs, pr_list_cli) +        else: +            fix_or_normal(commit_info, pr_list_bugs, pr_list_others) + +    git_expr = re.compile(r'^(?P<hash>[a-f0-9]+) (?P<title>.*) \(#(?P<pr>[0-9]+)\)$') +    for line in commit_list.stdout.split('\n'): +        match = git_expr.search(line) +        if match: +            pr_info = _get_pr_info(cache, gh, match.group("pr")) +            commit_info = {'hash': match.group("hash"), 'title': match.group("title"), 'pr_num': int(match.group("pr")), 'pr_labels': [label.name for label in pr_info.labels.items]} +            _categorise_commit(commit_info) + +    def _dump_commit_list(name, collection): +        if len(collection) == 0: +            return +        print("") +        print(f"{name}:") +        for commit in sorted(collection, key=lambda x: x['pr_num']): +            title = clean1_expr.sub('', clean2_expr.sub('', commit['title'])).strip() +            pr_num = commit['pr_num'] +            print(f'* {title} ([#{pr_num}](https://github.com/qmk/qmk_firmware/pull/{pr_num}))') + +    _dump_commit_list("Bugs", pr_list_bugs) +    _dump_commit_list("Core", pr_list_core) +    _dump_commit_list("CLI", pr_list_cli) +    _dump_commit_list("Submodule updates", pr_list_dependencies) +    _dump_commit_list("Keyboards", pr_list_keyboards) +    _dump_commit_list("Keyboard fixes", pr_list_keyboard_fixes) +    _dump_commit_list("Others", pr_list_others) diff --git a/lib/python/qmk/cli/generate/dfu_header.py b/lib/python/qmk/cli/generate/dfu_header.py index 5a1b109f1e..7fb585fc7d 100644 --- a/lib/python/qmk/cli/generate/dfu_header.py +++ b/lib/python/qmk/cli/generate/dfu_header.py @@ -32,7 +32,7 @@ def generate_dfu_header(cli):      keyboard_h_lines = ['/* This file was generated by `qmk generate-dfu-header`. Do not edit or copy.', ' */', '', '#pragma once']      keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}') -    keyboard_h_lines.append(f'#define PRODUCT {cli.config.generate_dfu_header.keyboard} Bootloader') +    keyboard_h_lines.append(f'#define PRODUCT {kb_info_json["keyboard_name"]} Bootloader')      # Optional      if 'qmk_lufa_bootloader.esc_output' in kb_info_json: diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index dcaff29fae..5d8d7cc8a7 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -67,12 +67,9 @@ def generate_rules_mk(cli):      # Iterate through features to enable/disable them      if 'features' in kb_info_json:          for feature, enabled in kb_info_json['features'].items(): -            if feature == 'bootmagic_lite' and enabled: -                rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite') -            else: -                feature = feature.upper() -                enabled = 'yes' if enabled else 'no' -                rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}') +            feature = feature.upper() +            enabled = 'yes' if enabled else 'no' +            rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')      # Set SPLIT_TRANSPORT, if needed      if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom': | 
