|  | #!/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) |