|  | # Copyright 2020 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  | """API for Bazel calls for config, cquery, and required fragment info. | 
|  |  | 
|  | There's no Python Bazel API so we invoke Bazel as a subprocess. | 
|  | """ | 
|  | import json | 
|  | import os | 
|  | import subprocess | 
|  | from typing import Callable | 
|  | from typing import List | 
|  | from typing import Tuple | 
|  | # Do not edit this line. Copybara replaces it with PY2 migration helper. | 
|  | from frozendict import frozendict | 
|  | from tools.ctexplain.types import Configuration | 
|  | from tools.ctexplain.types import ConfiguredTarget | 
|  | from tools.ctexplain.types import HostConfiguration | 
|  | from tools.ctexplain.types import NullConfiguration | 
|  |  | 
|  |  | 
|  | def run_bazel_in_client(args: List[str]) -> Tuple[int, List[str], List[str]]: | 
|  | """Calls bazel within the current workspace. | 
|  |  | 
|  | For production use. Tests use an alternative invoker that goes through test | 
|  | infrastructure. | 
|  |  | 
|  | Args: | 
|  | args: the arguments to call Bazel with | 
|  |  | 
|  | Returns: | 
|  | Tuple of (return code, stdout, stderr) | 
|  | """ | 
|  | result = subprocess.run( | 
|  | ["blaze"] + args, | 
|  | cwd=os.getcwd(), | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.PIPE, | 
|  | check=False) | 
|  | return (result.returncode, result.stdout.decode("utf-8").split(os.linesep), | 
|  | result.stderr) | 
|  |  | 
|  |  | 
|  | class BazelApi(): | 
|  | """API that accepts injectable Bazel invocation logic.""" | 
|  |  | 
|  | def __init__(self, | 
|  | run_bazel: Callable[[List[str]], | 
|  | Tuple[int, List[str], | 
|  | List[str]]] = run_bazel_in_client): | 
|  | self.run_bazel = run_bazel | 
|  |  | 
|  | def cquery(self, | 
|  | args: List[str]) -> Tuple[bool, str, Tuple[ConfiguredTarget, ...]]: | 
|  | """Calls cquery with the given arguments. | 
|  |  | 
|  | Args: | 
|  | args: A list of cquery command-line arguments, one argument per entry. | 
|  |  | 
|  | Returns: | 
|  | (success, stderr, cts), where success is True iff the query succeeded, | 
|  | stderr contains the query's stderr (regardless of success value), and cts | 
|  | is the configured targets found by the query if successful, empty | 
|  | otherwise. | 
|  |  | 
|  | ct order preserves cquery's output order. This is topologically sorted | 
|  | with duplicates removed. So no unique configured target appears twice and | 
|  | if A depends on B, A appears before B. | 
|  | """ | 
|  | base_args = ["cquery", "--show_config_fragments=transitive"] | 
|  | (returncode, stdout, stderr) = self.run_bazel(base_args + args) | 
|  | if returncode != 0: | 
|  | return (False, stderr, ()) | 
|  |  | 
|  | cts = [] | 
|  | for line in stdout: | 
|  | if not line.strip(): | 
|  | continue | 
|  | ctinfo = _parse_cquery_result_line(line) | 
|  | if ctinfo is not None: | 
|  | cts.append(ctinfo) | 
|  |  | 
|  | return (True, stderr, tuple(cts)) | 
|  |  | 
|  | def get_config(self, config_hash: str) -> Configuration: | 
|  | """Calls "bazel config" with the given config hash. | 
|  |  | 
|  | Args: | 
|  | config_hash: A config hash as reported by "bazel cquery". | 
|  |  | 
|  | Returns: | 
|  | The matching configuration or None if no match is found. | 
|  |  | 
|  | Raises: | 
|  | ValueError: On any parsing problems. | 
|  | """ | 
|  | if config_hash == "HOST": | 
|  | return HostConfiguration() | 
|  | elif config_hash == "null": | 
|  | return NullConfiguration() | 
|  |  | 
|  | base_args = ["config", "--output=json"] | 
|  | (returncode, stdout, stderr) = self.run_bazel(base_args + [config_hash]) | 
|  | if returncode != 0: | 
|  | raise ValueError("Could not get config: " + stderr) | 
|  | config_json = json.loads(os.linesep.join(stdout)) | 
|  | fragments = frozendict({ | 
|  | _base_name(entry["name"]): | 
|  | tuple(_base_name(clazz) for clazz in entry["fragmentOptions"]) | 
|  | for entry in config_json["fragments"] | 
|  | }) | 
|  | options = frozendict({ | 
|  | _base_name(entry["name"]): frozendict(entry["options"]) | 
|  | for entry in config_json["fragmentOptions"] | 
|  | }) | 
|  | return Configuration(fragments, options) | 
|  |  | 
|  |  | 
|  | # TODO(gregce): have cquery --output=jsonproto support --show_config_fragments | 
|  | # so we can replace all this regex parsing with JSON reads. | 
|  | def _parse_cquery_result_line(line: str) -> ConfiguredTarget: | 
|  | """Converts a cquery output line to a ConfiguredTarget. | 
|  |  | 
|  | Expected input is: | 
|  |  | 
|  | "<label> (<config hash>) [configFragment1, configFragment2, ...]" | 
|  |  | 
|  | or: | 
|  | "<label> (null)" | 
|  |  | 
|  | Args: | 
|  | line: The expected input. | 
|  |  | 
|  | Returns: | 
|  | Corresponding ConfiguredTarget if the line matches else None. | 
|  | """ | 
|  | tokens = line.split(maxsplit=2) | 
|  | label = tokens[0] | 
|  | if tokens[1][0] != "(" or tokens[1][-1] != ")": | 
|  | raise ValueError(f"{tokens[1]} in {line} not surrounded by parentheses") | 
|  | config_hash = tokens[1][1:-1] | 
|  | if config_hash == "null": | 
|  | fragments = () | 
|  | else: | 
|  | if tokens[2][0] != "[" or tokens[2][-1] != "]": | 
|  | raise ValueError(f"{tokens[2]} in {line} not surrounded by [] brackets") | 
|  | # The fragments list looks like '[Fragment1, Fragment2, ...]'. Split the | 
|  | # whole line on ' [' to get just this list, then remove the final ']', then | 
|  | # split again on ', ' to convert it to a structured tuple. | 
|  | fragments = tuple(line.split(" [")[1][0:-1].split(", ")) | 
|  | return ConfiguredTarget( | 
|  | label=label, | 
|  | config=None,  # Not yet available: we'll need `bazel config` to get this. | 
|  | config_hash=config_hash, | 
|  | transitive_fragments=fragments) | 
|  |  | 
|  |  | 
|  | def _base_name(full_name: str) -> str: | 
|  | """Strips a fully qualified Java class name to the file scope. | 
|  |  | 
|  | Examples: | 
|  | - "A.B.OuterClass" -> "OuterClass" | 
|  | - "A.B.OuterClass$InnerClass" -> "OuterClass$InnerClass" | 
|  |  | 
|  | Args: | 
|  | full_name: Fully qualified class name. | 
|  |  | 
|  | Returns: | 
|  | Stripped name. | 
|  | """ | 
|  | return full_name.split(".")[-1] |