1#!/usr/bin/env python3
2
3# Copyright 2020 gRPC authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17# This is based on the script on the Envoy project
18# https://github.com/envoyproxy/envoy/blob/master/tools/gen_compilation_database.py
19
20import argparse
21import glob
22import json
23import logging
24import os
25from pathlib import Path
26import re
27import shlex
28import subprocess
29
30RE_INCLUDE_SYSTEM = re.compile("\s*-I\s+/usr/[^ ]+")
31
32
33# This method is equivalent to https://github.com/grailbio/bazel-compilation-database/blob/master/generate.sh
34def generateCompilationDatabase(args):
35    # We need to download all remote outputs for generated source code.
36    # This option lives here to override those specified in bazelrc.
37    bazel_options = shlex.split(os.environ.get("BAZEL_BUILD_OPTIONS", "")) + [
38        "--config=compdb",
39        "--remote_download_outputs=all",
40    ]
41
42    subprocess.check_call(["bazel", "build"] + bazel_options + [
43        "--aspects=@bazel_compdb//:aspects.bzl%compilation_database_aspect",
44        "--output_groups=compdb_files,header_files"
45    ] + args.bazel_targets)
46
47    execroot = subprocess.check_output(["bazel", "info", "execution_root"] +
48                                       bazel_options).decode().strip()
49
50    compdb = []
51    for compdb_file in Path(execroot).glob("**/*.compile_commands.json"):
52        compdb.extend(
53            json.loads(
54                "[" +
55                compdb_file.read_text().replace("__EXEC_ROOT__", execroot) +
56                "]"))
57
58    if args.dedup_targets:
59        compdb_map = {target["file"]: target for target in compdb}
60        compdb = list(compdb_map.values())
61
62    return compdb
63
64
65def isHeader(filename):
66    for ext in (".h", ".hh", ".hpp", ".hxx"):
67        if filename.endswith(ext):
68            return True
69    return False
70
71
72def isCompileTarget(target, args):
73    filename = target["file"]
74    if not args.include_headers and isHeader(filename):
75        return False
76    if not args.include_genfiles:
77        if filename.startswith("bazel-out/"):
78            return False
79    if not args.include_external:
80        if filename.startswith("external/"):
81            return False
82    return True
83
84
85def modifyCompileCommand(target, args):
86    cc, options = target["command"].split(" ", 1)
87
88    # Workaround for bazel added C++14 options, those doesn't affect build itself but
89    # clang-tidy will misinterpret them.
90    options = options.replace("-std=c++0x ", "")
91    options = options.replace("-std=c++14 ", "")
92
93    # Add -DNDEBUG so that editors show the correct size information for structs.
94    options += " -DNDEBUG"
95
96    if args.vscode:
97        # Visual Studio Code doesn't seem to like "-iquote". Replace it with
98        # old-style "-I".
99        options = options.replace("-iquote ", "-I ")
100
101    if args.ignore_system_headers:
102        # Remove all include options for /usr/* directories
103        options = RE_INCLUDE_SYSTEM.sub("", options)
104
105    if isHeader(target["file"]):
106        options += " -Wno-pragma-once-outside-header -Wno-unused-const-variable"
107        options += " -Wno-unused-function"
108        if not target["file"].startswith("external/"):
109            # *.h file is treated as C header by default while our headers files are all C++14.
110            options = "-x c++ -std=c++14 -fexceptions " + options
111
112    target["command"] = " ".join([cc, options])
113    return target
114
115
116def fixCompilationDatabase(args, db):
117    db = [
118        modifyCompileCommand(target, args)
119        for target in db
120        if isCompileTarget(target, args)
121    ]
122
123    with open("compile_commands.json", "w") as db_file:
124        json.dump(db, db_file, indent=2)
125
126
127if __name__ == "__main__":
128    parser = argparse.ArgumentParser(
129        description='Generate JSON compilation database')
130    parser.add_argument('--include_external', action='store_true')
131    parser.add_argument('--include_genfiles', action='store_true')
132    parser.add_argument('--include_headers', action='store_true')
133    parser.add_argument('--vscode', action='store_true')
134    parser.add_argument('--ignore_system_headers', action='store_true')
135    parser.add_argument('--dedup_targets', action='store_true')
136    parser.add_argument('bazel_targets', nargs='*', default=["//..."])
137    args = parser.parse_args()
138    fixCompilationDatabase(args, generateCompilationDatabase(args))
139