create_compdb.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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 json
  22. import re
  23. import subprocess
  24. import sys
  25. from pathlib import Path
  26. import scripts_utils
  27. scripts_utils.chdir_repo_root()
  28. directory = Path.cwd()
  29. # We use the `BAZEL` environment variable if present. If not, then we try to
  30. # use `bazelisk` and then `bazel`.
  31. bazel = scripts_utils.locate_bazel()
  32. # Load compiler flags. We do this first in order to fail fast if not run from
  33. # the workspace root.
  34. print("Reading the arguments to use...")
  35. try:
  36. with open("compile_flags.txt") as flag_file:
  37. arguments = [line.strip() for line in flag_file]
  38. except FileNotFoundError:
  39. sys.exit(Path(sys.argv[0]).name + " must be run from the project root")
  40. # Prepend the `clang` executable path to the arguments that looks into our
  41. # downloaded Clang toolchain.
  42. arguments = ["clang++"] + arguments
  43. print("Building compilation database...")
  44. # Find all of the C++ source files that we expect to compile cleanly as
  45. # stand-alone files. This is a bit simpler than scraping the actual compile
  46. # actions and allows us to directly index header-only libraries easily and
  47. # pro-actively index the specific headers in the project.
  48. source_files_query = subprocess.check_output(
  49. [
  50. bazel,
  51. "query",
  52. "--keep_going",
  53. "--output=location",
  54. # Workaround for https://github.com/bazelbuild/bazel/issues/8900
  55. "--incompatible_display_source_file_location",
  56. 'filter(".*\\.(h|cpp|cc|c|cxx)$", kind("source file", deps(//...)))',
  57. ],
  58. stderr=subprocess.DEVNULL,
  59. universal_newlines=True,
  60. )
  61. source_files = [
  62. Path(line.split(":")[0]) for line in source_files_query.splitlines()
  63. ]
  64. # Filter into the Carbon source files that we'll find directly in the
  65. # workspace, and LLVM source files that need to be mapped through the merged
  66. # LLVM tree in Bazel's execution root.
  67. carbon_files = [
  68. f.relative_to(directory)
  69. for f in source_files
  70. if f.parts[: len(directory.parts)] == directory.parts
  71. ]
  72. llvm_files = [
  73. Path("bazel-execroot/external").joinpath(
  74. *f.parts[f.parts.index("llvm-project") :]
  75. )
  76. for f in source_files
  77. if "llvm-project" in f.parts
  78. ]
  79. print(
  80. "Found %d Carbon source files and %d LLVM source files..."
  81. % (len(carbon_files), len(llvm_files))
  82. )
  83. # Now collect the generated file labels.
  84. # cc_proto_library generates files, but they aren't seen with "generated file".
  85. generated_file_labels = subprocess.check_output(
  86. [
  87. bazel,
  88. "query",
  89. "--keep_going",
  90. "--output=label",
  91. (
  92. 'filter(".*\\.(h|cpp|cc|c|cxx|def|inc)$",'
  93. 'kind("generated file", deps(//...)))'
  94. " union "
  95. 'kind("cc_proto_library", deps(//...))'
  96. ),
  97. ],
  98. stderr=subprocess.DEVNULL,
  99. universal_newlines=True,
  100. ).splitlines()
  101. print("Found %d generated files..." % (len(generated_file_labels),))
  102. # Directly build these labels so that indexing can find them. Allow this to
  103. # fail in case there are build errors in the client, and just warn the user
  104. # that they may be missing generated files.
  105. print("Building the generated files so that tools can find them...")
  106. subprocess.check_call([bazel, "build", "--keep_going"] + generated_file_labels)
  107. # Also build some specific targets that depend on external packages so those are
  108. # fetched and linked into the Bazel execution root. We try to use cheap files
  109. # where possible, but in some cases need to create a virtual include directory.
  110. subprocess.check_call(
  111. [
  112. bazel,
  113. "build",
  114. "--keep_going",
  115. "@llvm-project//llvm:LICENSE.TXT",
  116. "@com_google_absl//:LICENSE",
  117. "@com_google_googletest//:LICENSE",
  118. "@com_googlesource_code_re2//:LICENSE",
  119. "@com_github_google_benchmark//:benchmark",
  120. "@com_google_libprotobuf_mutator//:LICENSE",
  121. "@com_google_protobuf//:any_proto",
  122. ]
  123. )
  124. # Manually translate the label to a user friendly path into the Bazel output
  125. # symlinks.
  126. def _label_to_path(s: str) -> Path:
  127. # Map external repositories to their part of the output tree.
  128. s = re.sub(r"^@([^/]+)//", r"bazel-bin/external/\1/", s)
  129. # Map this repository to the root of the output tree.
  130. s = s if not s.startswith("//") else "bazel-bin/" + s[len("//") :]
  131. # Replace the colon used to mark the package name with a slash.
  132. s = s.replace(":", "/")
  133. # Convert to a native path.
  134. return Path(s)
  135. generated_files = [_label_to_path(label) for label in generated_file_labels]
  136. # Generate compile_commands.json with an entry for each C++ input.
  137. entries = [
  138. {
  139. "directory": str(directory),
  140. "file": str(f),
  141. "arguments": arguments + [str(f)],
  142. }
  143. for f in carbon_files + llvm_files + generated_files
  144. ]
  145. with open("compile_commands.json", "w") as json_file:
  146. json.dump(entries, json_file, indent=2)