at master 5.7 kB view raw
1"""Creates a compilation database for the given keyboard build. 2""" 3 4import json 5import os 6import re 7import shlex 8import shutil 9from functools import lru_cache 10from pathlib import Path 11from typing import Dict, Iterator, List 12 13from milc import cli 14 15from qmk.commands import find_make 16from qmk.constants import QMK_FIRMWARE 17 18 19@lru_cache(maxsize=10) 20def system_libs(binary: str) -> List[Path]: 21 """Find the system include directory that the given build tool uses. 22 """ 23 cli.log.debug("searching for system library directory for binary: %s", binary) 24 25 # Actually query xxxxxx-gcc to find its include paths. 26 if binary.endswith("gcc") or binary.endswith("g++"): 27 # (TODO): Remove 'stdin' once 'input' no longer causes issues under MSYS 28 result = cli.run([binary, '-E', '-Wp,-v', '-'], capture_output=True, check=True, stdin=None, input='\n') 29 paths = [] 30 for line in result.stderr.splitlines(): 31 if line.startswith(" "): 32 paths.append(Path(line.strip()).resolve()) 33 return paths 34 35 return list(Path(binary).resolve().parent.parent.glob("*/include")) if binary else [] 36 37 38@lru_cache(maxsize=10) 39def cpu_defines(binary: str, compiler_args: str) -> List[str]: 40 cli.log.debug("gathering definitions for compilation: %s %s", binary, compiler_args) 41 if binary.endswith("gcc") or binary.endswith("g++"): 42 invocation = [binary, '-dM', '-E'] 43 if binary.endswith("gcc"): 44 invocation.extend(['-x', 'c', '-std=gnu11']) 45 elif binary.endswith("g++"): 46 invocation.extend(['-x', 'c++', '-std=gnu++14']) 47 invocation.extend(shlex.split(compiler_args)) 48 invocation.append('-') 49 result = cli.run(invocation, capture_output=True, check=True, stdin=None, input='\n') 50 define_args = [] 51 for line in result.stdout.splitlines(): 52 line_args = line.split(' ', 2) 53 if len(line_args) == 3 and line_args[0] == '#define': 54 define_args.append(f'-D{line_args[1]}={line_args[2]}') 55 elif len(line_args) == 2 and line_args[0] == '#define': 56 define_args.append(f'-D{line_args[1]}') 57 58 type_filter = re.compile( 59 r'^-D__(SIZE|INT|UINT|WINT|WCHAR|BYTE|SHRT|SIG|FLOAT|LONG|CHAR|SCHAR|DBL|FLT|LDBL|PTRDIFF|QQ|DQ|DA|HA|HQ|SA|SQ|TA|TQ|UDA|UDQ|UHA|UHQ|USQ|USA|UTQ|UTA|UQQ|UQA|ACCUM|FRACT|UACCUM|UFRACT|LACCUM|LFRACT|ULACCUM|ULFRACT|LLACCUM|LLFRACT|ULLACCUM|ULLFRACT|SACCUM|SFRACT|USACCUM|USFRACT)' 60 ) 61 return list(sorted(set(filter(lambda x: not type_filter.match(x), define_args)))) 62 return [] 63 64 65file_re = re.compile(r'printf "Compiling: ([^"]+)') 66cmd_re = re.compile(r'LOG=\$\((.+?)&&') 67 68 69def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]: 70 """parse the output of `make -n <target>` 71 72 This function makes many assumptions about the format of your build log. 73 This happens to work right now for qmk. 74 """ 75 76 state = 'start' 77 this_file = None 78 records = [] 79 for line in f: 80 if state == 'start': 81 m = file_re.search(line) 82 if m: 83 this_file = m.group(1) 84 state = 'cmd' 85 86 if state == 'cmd': 87 assert this_file 88 m = cmd_re.search(line) 89 if m: 90 # we have a hit! 91 this_cmd = m.group(1) 92 args = shlex.split(this_cmd) 93 binary = shutil.which(args[0]) 94 compiler_args = set(filter(lambda x: x.startswith('-m') or x.startswith('-f'), args)) 95 for s in system_libs(binary): 96 args += ['-isystem', '%s' % s] 97 args.extend(cpu_defines(binary, ' '.join(shlex.quote(s) for s in compiler_args))) 98 args[0] = binary 99 records.append({"arguments": args, "directory": str(QMK_FIRMWARE.resolve()), "file": this_file}) 100 state = 'start' 101 102 return records 103 104 105def write_compilation_database(keyboard: str = None, keymap: str = None, output_path: Path = QMK_FIRMWARE / 'compile_commands.json', skip_clean: bool = False, command: List[str] = None, **env_vars) -> bool: 106 # Generate the make command for a specific keyboard/keymap. 107 if not command: 108 from qmk.build_targets import KeyboardKeymapBuildTarget # Lazy load due to circular references 109 target = KeyboardKeymapBuildTarget(keyboard, keymap) 110 command = target.compile_command(dry_run=True, **env_vars) 111 112 if not command: 113 cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') 114 cli.echo('usage: qmk generate-compilation-database [-kb KEYBOARD] [-km KEYMAP]') 115 return False 116 117 # remove any environment variable overrides which could trip us up 118 env = os.environ.copy() 119 env.pop("MAKEFLAGS", None) 120 121 # re-use same executable as the main make invocation (might be gmake) 122 if not skip_clean: 123 clean_command = [find_make(), "clean"] 124 cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command)) 125 cli.run(clean_command, capture_output=False, check=True, env=env) 126 127 cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command)) 128 129 result = cli.run(command, capture_output=True, check=True, env=env) 130 db = parse_make_n(result.stdout.splitlines()) 131 if not db: 132 cli.log.error("Failed to parse output from make output:\n%s", result.stdout) 133 return False 134 135 cli.log.info("Found %s compile commands", len(db)) 136 137 cli.log.info(f"Writing build database to {output_path}") 138 output_path.write_text(json.dumps(db, indent=4)) 139 140 return True