|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +"""Create a compilation database for Clang tools like `clangd`. |
| 4 | +
|
| 5 | +If you want `clangd` to be able to index this project, run this script from |
| 6 | +the workspace root to generate a rich compilation database. After the first |
| 7 | +run, you should only need to run it if you encounter `clangd` problems, or if |
| 8 | +you want `clangd` to build an up-to-date index of the entire project. Note |
| 9 | +that in the latter case you may need to manually clear and rebuild clangd's |
| 10 | +index after running this script. |
| 11 | +
|
| 12 | +Note that this script will build generated files in the Carbon project and |
| 13 | +otherwise touch the Bazel build. It works to do the minimum amount necessary. |
| 14 | +Once setup, generally subsequent builds, even of small parts of the project, |
| 15 | +different configurations, or that hit errors won't disrupt things. But, if |
| 16 | +you do hit errors, you can get things back to a good state by fixing the |
| 17 | +build of generated files and re-running this script. |
| 18 | +""" |
| 19 | + |
| 20 | +__copyright__ = """ |
| 21 | +Part of the Carbon Language project, under the Apache License v2.0 with LLVM |
| 22 | +Exceptions. See /LICENSE for license information. |
| 23 | +SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 24 | +""" |
| 25 | + |
| 26 | +import json |
| 27 | +import os |
| 28 | +import re |
| 29 | +import shutil |
| 30 | +import subprocess |
| 31 | +import sys |
| 32 | +from pathlib import Path |
| 33 | + |
| 34 | +# Change the working directory to the repository root so that the remaining |
| 35 | +# operations reliably operate relative to that root. |
| 36 | +os.chdir(Path(__file__).parent.parent) |
| 37 | +directory = Path.cwd() |
| 38 | + |
| 39 | +# We use the `BAZEL` environment variable if present. If not, then we try to |
| 40 | +# use `bazelisk` and then `bazel`. |
| 41 | +bazel = os.environ.get("BAZEL") |
| 42 | +if not bazel: |
| 43 | + bazel = "bazelisk" |
| 44 | + if not shutil.which(bazel): |
| 45 | + bazel = "bazel" |
| 46 | + if not shutil.which(bazel): |
| 47 | + sys.exit("Unable to run Bazel") |
| 48 | + |
| 49 | +# Load compiler flags. We do this first in order to fail fast if not run from |
| 50 | +# the workspace root. |
| 51 | +print("Reading the arguments to use...") |
| 52 | +try: |
| 53 | + with open("compile_flags.txt") as flag_file: |
| 54 | + arguments = [line.strip() for line in flag_file] |
| 55 | +except FileNotFoundError: |
| 56 | + sys.exit(Path(sys.argv[0]).name + " must be run from the project root") |
| 57 | + |
| 58 | +# Prepend the `clang` executable path to the arguments that looks into our |
| 59 | +# downloaded Clang toolchain. |
| 60 | +arguments = [str(Path("bazel-clang-toolchain/bin/clang"))] + arguments |
| 61 | + |
| 62 | +print("Building compilation database...") |
| 63 | + |
| 64 | +# Find all of the C++ source files that we expect to compile cleanly as |
| 65 | +# stand-alone files. This is a bit simpler than scraping the actual compile |
| 66 | +# actions and allows us to directly index header-only libraries easily and |
| 67 | +# pro-actively index the specific headers in the project. |
| 68 | +source_files_query = subprocess.run( |
| 69 | + [ |
| 70 | + bazel, |
| 71 | + "query", |
| 72 | + "--keep_going", |
| 73 | + "--output=location", |
| 74 | + 'filter(".*\\.(h|cpp|cc|c|cxx)$", kind("source file", deps(//...)))', |
| 75 | + ], |
| 76 | + check=True, |
| 77 | + stdout=subprocess.PIPE, |
| 78 | + stderr=subprocess.DEVNULL, |
| 79 | + universal_newlines=True, |
| 80 | +).stdout |
| 81 | +source_files = [ |
| 82 | + Path(line.split(":")[0]) for line in source_files_query.splitlines() |
| 83 | +] |
| 84 | + |
| 85 | +# Filter into the Carbon source files that we'll find directly in the |
| 86 | +# workspace, and LLVM source files that need to be mapped through the merged |
| 87 | +# LLVM tree in Bazel's execution root. |
| 88 | +carbon_files = [ |
| 89 | + f.relative_to(directory) |
| 90 | + for f in source_files |
| 91 | + if f.parts[: len(directory.parts)] == directory.parts |
| 92 | +] |
| 93 | +llvm_files = [ |
| 94 | + Path("bazel-execroot/external").joinpath( |
| 95 | + *f.parts[f.parts.index("llvm-project") :] |
| 96 | + ) |
| 97 | + for f in source_files |
| 98 | + if "llvm-project" in f.parts |
| 99 | +] |
| 100 | +print( |
| 101 | + "Found %d Carbon source files and %d LLVM source files..." |
| 102 | + % (len(carbon_files), len(llvm_files)) |
| 103 | +) |
| 104 | + |
| 105 | +# Now collect the generated file labels. |
| 106 | +generated_file_labels = subprocess.run( |
| 107 | + [ |
| 108 | + bazel, |
| 109 | + "query", |
| 110 | + "--keep_going", |
| 111 | + "--output=label", |
| 112 | + ( |
| 113 | + 'filter(".*\\.(h|cpp|cc|c|cxx|def|inc)$",' |
| 114 | + 'kind("generated file", deps(//...)))' |
| 115 | + ), |
| 116 | + ], |
| 117 | + check=True, |
| 118 | + stdout=subprocess.PIPE, |
| 119 | + stderr=subprocess.DEVNULL, |
| 120 | + universal_newlines=True, |
| 121 | +).stdout.splitlines() |
| 122 | +print("Found %d generated files..." % (len(generated_file_labels),)) |
| 123 | + |
| 124 | +# Directly build these labels so that indexing can find them. Allow this to |
| 125 | +# fail in case there are build errors in the client, and just warn the user |
| 126 | +# that they may be missing generated files. |
| 127 | +print("Building the generated files so that tools can find them...") |
| 128 | +subprocess.run([bazel, "build", "--keep_going"] + generated_file_labels) |
| 129 | + |
| 130 | + |
| 131 | +# Manually translate the label to a user friendly path into the Bazel output |
| 132 | +# symlinks. |
| 133 | +def _label_to_path(s): |
| 134 | + # Map external repositories to their part of the output tree. |
| 135 | + s = re.sub(r"^@([^/]+)//", r"bazel-bin/external/\1/", s) |
| 136 | + # Map this repository to the root of the output tree. |
| 137 | + s = s if not s.startswith("//") else "bazel-bin/" + s[len("//") :] |
| 138 | + # Replace the colon used to mark the package name with a slash. |
| 139 | + s = s.replace(":", "/") |
| 140 | + # Convert to a native path. |
| 141 | + return Path(s) |
| 142 | + |
| 143 | + |
| 144 | +generated_files = [_label_to_path(label) for label in generated_file_labels] |
| 145 | + |
| 146 | +# Generate compile_commands.json with an entry for each C++ input. |
| 147 | +entries = [ |
| 148 | + { |
| 149 | + "directory": str(directory), |
| 150 | + "file": str(f), |
| 151 | + "arguments": arguments + [str(f)], |
| 152 | + } |
| 153 | + for f in carbon_files + llvm_files + generated_files |
| 154 | +] |
| 155 | +with open("compile_commands.json", "w") as json_file: |
| 156 | + json.dump(entries, json_file, indent=2) |
0 commit comments