|  | #!/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 | 
|  |  | 
|  | Tip: If your IDE complains that it cannot find headers, e.g. "gmock/gmock.h", it | 
|  | might be telling the truth. Try building the relevant target with Bazel | 
|  | (specifying "--config=riscv32" as necessary) and then restart clangd. | 
|  |  | 
|  | [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 logging | 
|  | import os | 
|  | import subprocess | 
|  | import sys | 
|  | from typing import Dict, List, Tuple | 
|  |  | 
|  | logger = logging.getLogger('generate_compilation_db') | 
|  |  | 
|  |  | 
|  | 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'] | 
|  |  | 
|  | # For our purposes, `os.sep.join()` should be equivalent to | 
|  | # `os.path.join()`, but without the additional overhead. | 
|  | return os.sep.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`.""" | 
|  | SOURCE_EXTENSIONS = [".h", ".c", ".cc"] | 
|  |  | 
|  | 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) | 
|  | if path.startswith("external/"): | 
|  | continue | 
|  | if not any(path.endswith(ext) for ext in SOURCE_EXTENSIONS): | 
|  | continue | 
|  | 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 transform_arguments_for_clangd(self) -> List[str]: | 
|  | """Return modified arguments for compatibility with Clangd. | 
|  |  | 
|  | It appears that Clangd fails to infer the desired target from the | 
|  | compiler name. For instance, this is the path to our cross-compiler: | 
|  | `external/crt/toolchains/lowrisc_rv32imcb/wrappers/clang`. Specifically, | 
|  | Clangd fails to launch a compiler instance if it sees `--march=rv32imc` | 
|  | or `--mabi=ilp32`. | 
|  |  | 
|  | This function explicitly tells Clangd which target we want by inserting | 
|  | a `--target=riscv32` flag as needed. | 
|  | """ | 
|  | args = self.arguments | 
|  | if not args: | 
|  | return args | 
|  | compiler_path = args[0] | 
|  | if 'lowrisc_rv32imcb' in compiler_path: | 
|  | return [compiler_path, '--target=riscv32'] + args[1:] | 
|  | return args | 
|  |  | 
|  |  | 
|  | class PathBuilder: | 
|  | """Helper class that builds useful paths relative to this source file.""" | 
|  |  | 
|  | def __init__(self, script_path): | 
|  | util_dir = os.path.dirname(script_path) | 
|  | self.top_dir = os.path.dirname(util_dir) | 
|  | if self.top_dir == '': | 
|  | raise Exception('Could not find parent of the util directory.') | 
|  | self.bazelisk_script = os.path.join(self.top_dir, 'bazelisk.sh') | 
|  | # Bazel creates a symlink to execRoot based on the workspace name. | 
|  | # https://bazel.build/remote/output-directories | 
|  | self.bazel_exec_root = os.path.join( | 
|  | self.top_dir, f"bazel-{os.path.basename(self.top_dir)}") | 
|  |  | 
|  |  | 
|  | def build_compile_commands( | 
|  | paths: PathBuilder, | 
|  | device_build: bool) -> Tuple[List[Dict], List[Dict]]: | 
|  | bazel_aquery_command = [ | 
|  | paths.bazelisk_script, | 
|  | 'aquery', | 
|  | '--output=jsonproto', | 
|  | ] | 
|  | if device_build: | 
|  | bazel_aquery_command.append('--config=riscv32') | 
|  | bazel_aquery_command.append(f'mnemonic("CppCompile", {args.target})') | 
|  |  | 
|  | logger.info("Running bazel command: %s", bazel_aquery_command) | 
|  | try: | 
|  | completed_process = subprocess.run(bazel_aquery_command, | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.PIPE, | 
|  | check=True) | 
|  | except subprocess.CalledProcessError as e: | 
|  | print(e.stderr.decode('utf-8'), file=sys.stderr) | 
|  | raise | 
|  | except BaseException: | 
|  | raise | 
|  |  | 
|  | logger.info("Processing output from bazel aquery") | 
|  | aquery_results = BazelAqueryResults( | 
|  | completed_process.stdout.decode('utf-8')) | 
|  |  | 
|  | compile_commands = [] | 
|  | unittest_compile_commands = [] | 
|  | for action in aquery_results.actions: | 
|  | assert action.mnemonic == 'CppCompile' | 
|  | assert action.arguments != [] | 
|  |  | 
|  | arguments = action.transform_arguments_for_clangd() | 
|  |  | 
|  | for artifact in aquery_results.iter_artifacts_for_dep_sets( | 
|  | action.input_dep_set_ids): | 
|  | command = { | 
|  | 'directory': paths.bazel_exec_root, | 
|  | 'arguments': arguments, | 
|  | 'file': artifact, | 
|  | } | 
|  |  | 
|  | if artifact.endswith("_unittest.cc"): | 
|  | unittest_compile_commands.append(command) | 
|  | else: | 
|  | compile_commands.append(command) | 
|  |  | 
|  | return (compile_commands, unittest_compile_commands) | 
|  |  | 
|  |  | 
|  | def main(args): | 
|  | paths = PathBuilder(os.path.realpath(__file__)) | 
|  |  | 
|  | device_commands, device_unittest_commands = build_compile_commands( | 
|  | paths, device_build=True) | 
|  | host_commands, host_unittest_commands = build_compile_commands( | 
|  | paths, device_build=False) | 
|  |  | 
|  | # In case there are conflicting host and device commands for "*_unittest.cc" | 
|  | # sources, we strategically place the host commands first. Conversely, we | 
|  | # favor the device commands for non-test sources. | 
|  | all_compile_commands = device_commands + host_commands + \ | 
|  | host_unittest_commands + device_unittest_commands | 
|  |  | 
|  | logger.info("Writing compile commands to %s", args.out) | 
|  | compile_commands_json = json.dumps(all_compile_commands, 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.') | 
|  |  | 
|  | if len(sys.argv) == 1: | 
|  | parser.print_help() | 
|  | sys.exit(1) | 
|  |  | 
|  | args = parser.parse_args() | 
|  |  | 
|  | logging.basicConfig(format='%(asctime)s %(message)s') | 
|  | logger.setLevel(logging.DEBUG) | 
|  |  | 
|  | main(args) |