create_compdb.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #!/usr/bin/env python3
  2. """Create a compilation database for Clang tools like `clangd`.
  3. If you want `clangd` to be able to index this project, run this script from
  4. the workspace root to generate a rich compilation database. After the first
  5. run, you should only need to run it if you encounter `clangd` problems, or if
  6. you want `clangd` to build an up-to-date index of the entire project. Note
  7. that in the latter case you may need to manually clear and rebuild clangd's
  8. index after running this script.
  9. Note that this script will build generated files in the Carbon project and
  10. otherwise touch the Bazel build. It works to do the minimum amount necessary.
  11. Once setup, generally subsequent builds, even of small parts of the project,
  12. different configurations, or that hit errors won't disrupt things. But, if
  13. you do hit errors, you can get things back to a good state by fixing the
  14. build of generated files and re-running this script.
  15. """
  16. __copyright__ = """
  17. Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  18. Exceptions. See /LICENSE for license information.
  19. SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  20. """
  21. import argparse
  22. import json
  23. import subprocess
  24. import sys
  25. from typing import Any, Dict
  26. import scripts_utils
  27. def _build_generated_files(
  28. bazel: str,
  29. logtostderr: bool,
  30. dump_files: bool,
  31. extra_bazel_flags: list[str] = [],
  32. ) -> None:
  33. print("Building the generated files so that tools can find them...")
  34. # Collect the generated file labels. Include some rules which generate
  35. # files but aren't classified as "generated file".
  36. kinds_query = (
  37. "filter("
  38. ' ".*\\.(h|hpp|hxx|cpp|cc|c|cxx|def|inc|s|S)$",'
  39. ' kind("(.*generate.*|manifest_as_cpp)",'
  40. # tree_sitter is excluded here because it causes the query to failure on
  41. # `@platforms`.
  42. " deps(//... except //utils/tree_sitter/...))"
  43. ")"
  44. )
  45. log_to = None
  46. if not logtostderr:
  47. log_to = subprocess.DEVNULL
  48. generated_file_labels = subprocess.check_output(
  49. [bazel, "query"]
  50. + extra_bazel_flags
  51. + ["--keep_going", "--output=label", kinds_query],
  52. stderr=log_to,
  53. encoding="utf-8",
  54. ).splitlines()
  55. if dump_files:
  56. for f in sorted(generated_file_labels):
  57. print(f)
  58. sys.exit(0)
  59. print(f"Found {len(generated_file_labels)} generated files...", flush=True)
  60. # Directly build these labels so that indexing can find them. Allow this to
  61. # fail in case there are build errors in the client, and just warn the user
  62. # that they may be missing generated files.
  63. subprocess.check_call(
  64. [bazel, "build"]
  65. + extra_bazel_flags
  66. + ["--keep_going", "--remote_download_outputs=toplevel"]
  67. + generated_file_labels
  68. # We also need the Bazel C++ runfiles that aren't "generated", but are
  69. # not linked into place until built.
  70. + ["@bazel_tools//tools/cpp/runfiles:runfiles"]
  71. )
  72. def _get_config_for_entry(entry: Dict[str, Any]) -> str:
  73. """Returns the configuration for a compile command entry."""
  74. arguments = entry.get("arguments")
  75. # Only handle files where the object file argument is easily found as
  76. # the last argument, which matches the expected structure from Bazel.
  77. if not arguments or len(arguments) < 2 or arguments[-2] != "-o":
  78. return "unknown"
  79. obj_file = arguments[-1]
  80. # The configuration is the name of the subdirectory of `bazel-out`.
  81. if not obj_file.startswith("bazel-out/"):
  82. return "unknown"
  83. return str(obj_file.split("/")[1])
  84. def _filter_compilation_database(file_path: str) -> None:
  85. """Filters out duplicate exec-config entries from the database."""
  86. print("Filtering out duplicate exec-configuration entries...")
  87. try:
  88. with open(file_path, "r") as f:
  89. commands = json.load(f)
  90. except FileNotFoundError:
  91. print(f"Error: The file '{file_path}' was not found.")
  92. sys.exit(1)
  93. except json.JSONDecodeError:
  94. print(f"Error: The file '{file_path}' is not a valid JSON file.")
  95. sys.exit(1)
  96. # We want to skip compiles that were in the "exec" configuration for tools.
  97. # Because we generate compile commands for every bazel cc_* target in the
  98. # main configuration, even if only used by tools, their sources should be
  99. # covered and the exec configuration would simply be a duplicate.
  100. #
  101. # Detecting this based on the `-exec-` string in the configuration name of
  102. # the directory is a bit of a hack, but even using the `--notool_deps`
  103. # argument, Bazel seems to sometimes include this configuration in the query
  104. # that produces the compilation database.
  105. filtered_commands = [
  106. entry
  107. for entry in commands
  108. if "-exec-" not in _get_config_for_entry(entry)
  109. ]
  110. with open(file_path, "w") as f:
  111. # Use indent=4 for a human-readable, pretty-printed output file
  112. json.dump(filtered_commands, f, indent=4)
  113. print(
  114. "Filtered out "
  115. f"{len(commands) - len(filtered_commands)} "
  116. "duplicate entries..."
  117. )
  118. def main() -> None:
  119. parser = argparse.ArgumentParser(
  120. description=__doc__,
  121. allow_abbrev=False,
  122. )
  123. parser.add_argument(
  124. "--alsologtostderr",
  125. action="store_true",
  126. help="Prints subcommand errors to stderr (default: False)",
  127. )
  128. parser.add_argument(
  129. "--dump-files",
  130. action="store_true",
  131. help="Dumps the full list of generated files (default: False)",
  132. )
  133. parser.add_argument(
  134. "--extra-bazel-flag",
  135. action="append",
  136. default=[],
  137. help=(
  138. "Extra flag to pass to Bazel invocations, may be specified more "
  139. "than once"
  140. ),
  141. )
  142. args = parser.parse_args()
  143. scripts_utils.chdir_repo_root()
  144. bazel = scripts_utils.locate_bazel()
  145. _build_generated_files(
  146. bazel, args.alsologtostderr, args.dump_files, args.extra_bazel_flag
  147. )
  148. print(
  149. "Generating compile_commands.json (may take a few minutes)...",
  150. flush=True,
  151. )
  152. subprocess.run(
  153. [
  154. bazel,
  155. "run",
  156. ]
  157. + args.extra_bazel_flag
  158. + [
  159. "@hedron_compile_commands//:refresh_all",
  160. "--",
  161. ]
  162. + args.extra_bazel_flag
  163. + [
  164. "--notool_deps",
  165. ]
  166. )
  167. _filter_compilation_database("compile_commands.json")
  168. if __name__ == "__main__":
  169. main()