| #!/usr/bin/env python3 | |
| # Copyright lowRISC contributors. | |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """generate_compilation_db.py builds compilation_commands.json from BUILD files. | |
| This tool runs a Bazel Action Graph query (Bazel's "aquery" command) and | |
| transforms the results to produce a compilation database (aka | |
| compile_commands.json). The goal is to enable semantic features like | |
| jump-to-definition and cross-references in IDEs that support | |
| compile_commands.json. | |
| The analysis.ActionGraphContainer protobuf [0] defines aquery's results format. | |
| Clang informally defines the schema of compile_commands.json [1]. | |
| Caveat: this tool only emits the commands for building C/C++ code. | |
| Example: | |
| util/generate_compilation_db.py --target //sw/... --out compile_commands.json | |
| [0]: https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/analysis_v2.proto | |
| [1]: https://clang.llvm.org/docs/JSONCompilationDatabase.html | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import subprocess | |
| from typing import Dict, List | |
| def build_id_lookup_dict(dicts: List[Dict]): | |
| """Create a dict from `dicts` indexed by the "id" key.""" | |
| lookup = {} | |
| for d in dicts: | |
| lookup[d['id']] = d | |
| return lookup | |
| class BazelAqueryResults: | |
| """Corresponds to Bazel's analysis.ActionGraphContainer protobuf.""" | |
| def __init__(self, output: str): | |
| parsed = json.loads(output) | |
| self.actions = [ | |
| BazelAqueryAction(action) for action in parsed['actions'] | |
| ] | |
| self.dep_sets_ = build_id_lookup_dict(parsed['depSetOfFiles']) | |
| self.artifacts_ = build_id_lookup_dict(parsed['artifacts']) | |
| self.path_fragments_ = build_id_lookup_dict(parsed['pathFragments']) | |
| def reconstruct_path(self, id: int): | |
| """Reconstruct a file path from Bazel aquery fragments.""" | |
| labels = [] | |
| while True: | |
| path_fragment = self.path_fragments_[id] | |
| labels.append(path_fragment['label']) | |
| if 'parentId' not in path_fragment: | |
| break | |
| id = path_fragment['parentId'] | |
| return os.path.join(*reversed(labels)) | |
| def iter_artifacts_for_dep_sets(self, dep_set_ids: List[int]): | |
| """Iterate the reconstructed paths of all artifacts related to `dep_set_ids`.""" | |
| dep_set_id_stack = dep_set_ids | |
| while len(dep_set_id_stack) > 0: | |
| dep_set_id = dep_set_id_stack.pop() | |
| dep_set = self.dep_sets_[dep_set_id] | |
| for direct_artifact_id in dep_set.get('directArtifactIds', []): | |
| artifact = self.artifacts_[direct_artifact_id] | |
| path_fragment_id = artifact['pathFragmentId'] | |
| path = self.reconstruct_path(path_fragment_id) | |
| yield path | |
| for transitive_dep_set_id in dep_set.get('transitiveDepSetIds', | |
| []): | |
| dep_set_id_stack.append(transitive_dep_set_id) | |
| class BazelAqueryAction: | |
| """Corresponds to Bazel's analysis.Action protobuf.""" | |
| def __init__(self, action: Dict): | |
| self.mnemonic = action.get('mnemonic', None) | |
| self.arguments = action.get('arguments', None) | |
| self.input_dep_set_ids = action.get('inputDepSetIds', []) | |
| def main(args): | |
| script_path = os.path.realpath(__file__) | |
| utils_dir = os.path.dirname(script_path) | |
| top_dir = os.path.dirname(utils_dir) | |
| bazel_aquery_command = [ | |
| os.path.join(top_dir, 'bazelisk.sh'), | |
| 'aquery', | |
| '--output=jsonproto', | |
| args.target, | |
| ] | |
| completed_process = subprocess.run(bazel_aquery_command, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| check=True) | |
| aquery_results = BazelAqueryResults(completed_process.stdout) | |
| compile_commands = [] | |
| for action in aquery_results.actions: | |
| if action.mnemonic != 'CppCompile' or action.arguments == []: | |
| continue | |
| for artifact in aquery_results.iter_artifacts_for_dep_sets( | |
| action.input_dep_set_ids): | |
| compile_commands.append({ | |
| 'directory': | |
| os.path.join(top_dir, "bazel-opentitan"), | |
| 'arguments': | |
| action.arguments, | |
| 'file': | |
| artifact, | |
| }) | |
| compile_commands_json = json.dumps(compile_commands, | |
| sort_keys=True, | |
| indent=4) | |
| if not args.out: | |
| print(compile_commands_json) | |
| return | |
| with open(args.out, 'w') as output_file: | |
| output_file.write(compile_commands_json) | |
| if __name__ == '__main__': | |
| parser = argparse.ArgumentParser( | |
| description=__doc__, formatter_class=argparse.RawTextHelpFormatter) | |
| parser.add_argument('--target', | |
| default='//...', | |
| help='Bazel target. Default is "//...".') | |
| parser.add_argument( | |
| '--out', | |
| help='Path of output file for compilation DB. Defaults to stdout.') | |
| args = parser.parse_args() | |
| main(args) |