compilation_database.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. """Creates a compilation database for the given keyboard build.
  2. """
  3. import itertools
  4. import json
  5. import os
  6. import re
  7. import shlex
  8. import shutil
  9. from functools import lru_cache
  10. from pathlib import Path
  11. from typing import Dict, Iterator, List, Union
  12. from milc import cli, MILC
  13. from qmk.commands import create_make_command
  14. from qmk.constants import QMK_FIRMWARE
  15. from qmk.decorators import automagic_keyboard, automagic_keymap
  16. @lru_cache(maxsize=10)
  17. def system_libs(binary: str) -> List[Path]:
  18. """Find the system include directory that the given build tool uses.
  19. """
  20. cli.log.debug("searching for system library directory for binary: %s", binary)
  21. bin_path = shutil.which(binary)
  22. return list(Path(bin_path).resolve().parent.parent.glob("*/include")) if bin_path else []
  23. file_re = re.compile(r'printf "Compiling: ([^"]+)')
  24. cmd_re = re.compile(r'LOG=\$\((.+?)&&')
  25. def parse_make_n(f: Iterator[str]) -> List[Dict[str, str]]:
  26. """parse the output of `make -n <target>`
  27. This function makes many assumptions about the format of your build log.
  28. This happens to work right now for qmk.
  29. """
  30. state = 'start'
  31. this_file = None
  32. records = []
  33. for line in f:
  34. if state == 'start':
  35. m = file_re.search(line)
  36. if m:
  37. this_file = m.group(1)
  38. state = 'cmd'
  39. if state == 'cmd':
  40. assert this_file
  41. m = cmd_re.search(line)
  42. if m:
  43. # we have a hit!
  44. this_cmd = m.group(1)
  45. args = shlex.split(this_cmd)
  46. args += ['-I%s' % s for s in system_libs(args[0])]
  47. new_cmd = ' '.join(shlex.quote(s) for s in args if s != '-mno-thumb-interwork')
  48. records.append({"directory": str(QMK_FIRMWARE.resolve()), "command": new_cmd, "file": this_file})
  49. state = 'start'
  50. return records
  51. @cli.argument('-kb', '--keyboard', help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
  52. @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
  53. @cli.subcommand('Create a compilation database.')
  54. @automagic_keyboard
  55. @automagic_keymap
  56. def generate_compilation_database(cli: MILC) -> Union[bool, int]:
  57. """Creates a compilation database for the given keyboard build.
  58. Does a make clean, then a make -n for this target and uses the dry-run output to create
  59. a compilation database (compile_commands.json). This file can help some IDEs and
  60. IDE-like editors work better. For more information about this:
  61. https://clang.llvm.org/docs/JSONCompilationDatabase.html
  62. """
  63. command = None
  64. # check both config domains: the magic decorator fills in `generate_compilation_database` but the user is
  65. # more likely to have set `compile` in their config file.
  66. current_keyboard = cli.config.generate_compilation_database.keyboard or cli.config.user.keyboard
  67. current_keymap = cli.config.generate_compilation_database.keymap or cli.config.user.keymap
  68. if current_keyboard and current_keymap:
  69. # Generate the make command for a specific keyboard/keymap.
  70. command = create_make_command(current_keyboard, current_keymap, dry_run=True)
  71. elif not current_keyboard:
  72. cli.log.error('Could not determine keyboard!')
  73. elif not current_keymap:
  74. cli.log.error('Could not determine keymap!')
  75. if not command:
  76. cli.log.error('You must supply both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
  77. cli.echo('usage: qmk compiledb [-kb KEYBOARD] [-km KEYMAP]')
  78. return False
  79. # remove any environment variable overrides which could trip us up
  80. env = os.environ.copy()
  81. env.pop("MAKEFLAGS", None)
  82. # re-use same executable as the main make invocation (might be gmake)
  83. clean_command = [command[0], 'clean']
  84. cli.log.info('Making clean with {fg_cyan}%s', ' '.join(clean_command))
  85. cli.run(clean_command, capture_output=False, check=True, env=env)
  86. cli.log.info('Gathering build instructions from {fg_cyan}%s', ' '.join(command))
  87. result = cli.run(command, capture_output=True, check=True, env=env)
  88. db = parse_make_n(result.stdout.splitlines())
  89. if not db:
  90. cli.log.error("Failed to parse output from make output:\n%s", result.stdout)
  91. return False
  92. cli.log.info("Found %s compile commands", len(db))
  93. dbpath = QMK_FIRMWARE / 'compile_commands.json'
  94. cli.log.info(f"Writing build database to {dbpath}")
  95. dbpath.write_text(json.dumps(db, indent=4))
  96. return True