| # 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. | |
| r"""Command line diffing tool that compares two bazel aquery invocations. | |
| This script compares the proto or textproto output of two bazel aquery | |
| invocations. For each set of output files of an action, it compares the command | |
| lines that generated the files. | |
| Example usage: | |
| 1. Prepare 2 aquery output files: | |
| bazel aquery //path/to:target_one --output=textproto > \ | |
| /path/to/output_one.textproto | |
| bazel aquery //path/to:target_two --output=textproto > \ | |
| /path/to/output_two.textproto | |
| 2. Run the differ from a bazel repo: | |
| bazel run //scripts/aquery_differ:aquery_differ -- \ | |
| --before=/path/to/output_one.textproto \ | |
| --after=/path/to/output_two.textproto \ | |
| --input_type=textproto \ | |
| --attrs=cmdline \ | |
| --attrs=inputs | |
| """ | |
| import difflib | |
| import os | |
| import sys | |
| from absl import app | |
| from absl import flags | |
| from google.protobuf import proto | |
| from google.protobuf import text_format | |
| from scripts.aquery_differ.resolvers.dep_set_resolver import DepSetResolver | |
| from scripts.aquery_differ.resolvers.path_fragment_resolver import PathFragmentResolver | |
| from src.main.protobuf import analysis_v2_pb2 | |
| # pylint: disable=g-import-not-at-top | |
| # resource lib isn't available on Windows. | |
| if os.name != "nt": | |
| import resource | |
| # pylint: enable=g-import-not-at-top | |
| flags.DEFINE_string("before", None, "Aquery output before the change") | |
| flags.DEFINE_string("after", None, "Aquery output after the change") | |
| flags.DEFINE_enum( | |
| "input_type", | |
| "proto", | |
| ["proto", "textproto", "streamed_proto"], | |
| "The format of the aquery proto input. One of 'proto', 'textproto' and" | |
| " 'streamed_proto'.", | |
| ) | |
| flags.DEFINE_multi_enum( | |
| "attrs", | |
| ["cmdline"], | |
| ["inputs", "cmdline"], | |
| "Attributes of the actions to be compared.", | |
| ) | |
| flags.DEFINE_integer( | |
| "max_mem_alloc_mb", | |
| 3072, | |
| "Amount of max memory available for aquery_differ, in MB.", | |
| ) | |
| flags.mark_flag_as_required("before") | |
| flags.mark_flag_as_required("after") | |
| WHITE = "\033[37m%s\033[0m" | |
| CYAN = "\033[36m%s\033[0m" | |
| RED = "\033[31m%s\033[0m" | |
| GREEN = "\033[32m%s\033[0m" | |
| def _colorize(line): | |
| """Add color to the input string.""" | |
| if not sys.stdout.isatty(): | |
| return line | |
| if line.startswith("+++") or line.startswith("---"): | |
| return WHITE % line | |
| if line.startswith("@@"): | |
| return CYAN % line | |
| if line.startswith("+"): | |
| return GREEN % line | |
| if line.startswith("-"): | |
| return RED % line | |
| return line | |
| def _print_diff( | |
| output_files, before_val, after_val, attr, before_file, after_file | |
| ): | |
| """Prints the unified diff between before_val and after_val.""" | |
| diff = "\n".join( | |
| map( | |
| _colorize, | |
| [ | |
| s.strip("\n") | |
| for s in difflib.unified_diff( | |
| before_val, after_val, before_file, after_file | |
| ) | |
| ], | |
| ) | |
| ) | |
| print( | |
| ( | |
| "[%s]\n" | |
| "Difference in the action that generates the following output(s):" | |
| "\n\t%s\n%s\n" | |
| ) | |
| % (attr, "\n\t".join(output_files.split()), diff) | |
| ) | |
| def _map_artifact_id_to_path(artifacts, path_fragments): | |
| path_fragment_resolver = PathFragmentResolver(path_fragments) | |
| return { | |
| artifact.id: path_fragment_resolver.resolve(artifact.path_fragment_id) | |
| for artifact in artifacts | |
| } | |
| def _map_action_index_to_output_files(actions, artifacts): | |
| """Constructs a map from action index to output files. | |
| Args: | |
| actions: a list of actions from the action graph container | |
| artifacts: a map {artifact_id: artifact path} | |
| Returns: | |
| A map from action index (in action graph container) to a string of | |
| concatenated output artifacts paths. | |
| """ | |
| action_index_to_output_files = {} | |
| for i, action in enumerate(actions): | |
| output_files = " ".join( | |
| sorted([artifacts[output_id] for output_id in action.output_ids]) | |
| ) | |
| action_index_to_output_files[i] = output_files | |
| return action_index_to_output_files | |
| # output files -> input artifacts | |
| def _map_output_files_to_input_artifacts( | |
| action_graph_container, artifact_id_to_path, action_index_to_output_files | |
| ): | |
| """Constructs a map from output files to input artifacts. | |
| Args: | |
| action_graph_container: the full action graph container object | |
| artifact_id_to_path: a map {artifact_id: artifact path} | |
| action_index_to_output_files: a map from action index (in action graph | |
| container) to a string of concatenated output artifacts paths. | |
| Returns: | |
| A map from output files (string of concatenated output artifacts paths) to a | |
| list of input artifacts. | |
| """ | |
| actions = action_graph_container.actions | |
| dep_set_of_files = action_graph_container.dep_set_of_files | |
| id_to_dep_set = {dep_set.id: dep_set for dep_set in dep_set_of_files} | |
| dep_set_resolver = DepSetResolver(dep_set_of_files, artifact_id_to_path) | |
| output_files_to_input_artifacts = {} | |
| for i, action in enumerate(actions): | |
| input_artifacts = set() | |
| for dep_set_id in action.input_dep_set_ids: | |
| input_artifacts.update( | |
| dep_set_resolver.resolve(id_to_dep_set[dep_set_id]) | |
| ) | |
| output_files_to_input_artifacts[action_index_to_output_files[i]] = sorted( | |
| list(input_artifacts) | |
| ) | |
| return output_files_to_input_artifacts | |
| # output files -> command line | |
| def _map_output_files_to_command_line(actions, action_index_to_output_files): | |
| """Constructs a map from output files to command line. | |
| Args: | |
| actions: a list of actions from the action graph container | |
| action_index_to_output_files: a map from action index (in action graph | |
| container) to a string of concatenated output artifacts paths. | |
| Returns: | |
| A map from output files (string of concatenated output artifacts paths) | |
| to the command line (a list of arguments). | |
| """ | |
| output_files_to_command_line = {} | |
| for i, action in enumerate(actions): | |
| output_files_to_command_line[action_index_to_output_files[i]] = ( | |
| action.arguments | |
| ) | |
| return output_files_to_command_line | |
| def _aquery_diff(before_proto, after_proto, attrs, before_file, after_file): | |
| """Returns differences between command lines that generate same outputs.""" | |
| found_difference = False | |
| artifacts_before = _map_artifact_id_to_path( | |
| before_proto.artifacts, before_proto.path_fragments | |
| ) | |
| artifacts_after = _map_artifact_id_to_path( | |
| after_proto.artifacts, after_proto.path_fragments | |
| ) | |
| action_to_output_files_before = _map_action_index_to_output_files( | |
| before_proto.actions, artifacts_before | |
| ) | |
| action_to_output_files_after = _map_action_index_to_output_files( | |
| after_proto.actions, artifacts_after | |
| ) | |
| # There's a 1-to-1 mapping between action and outputs | |
| output_files_before = set(action_to_output_files_before.values()) | |
| output_files_after = set(action_to_output_files_after.values()) | |
| before_after_diff = output_files_before - output_files_after | |
| after_before_diff = output_files_after - output_files_before | |
| if before_after_diff: | |
| print( | |
| ( | |
| "Aquery output 'before' change contains an action that generates " | |
| "the following outputs that aquery output 'after' change doesn't:" | |
| "\n%s\n" | |
| ) | |
| % "\n".join(before_after_diff) | |
| ) | |
| found_difference = True | |
| if after_before_diff: | |
| print( | |
| ( | |
| "Aquery output 'after' change contains an action that generates " | |
| "the following outputs that aquery output 'before' change doesn't:" | |
| "\n%s\n" | |
| ) | |
| % "\n".join(after_before_diff) | |
| ) | |
| found_difference = True | |
| if "cmdline" in attrs: | |
| output_to_command_line_before = _map_output_files_to_command_line( | |
| before_proto.actions, action_to_output_files_before | |
| ) | |
| output_to_command_line_after = _map_output_files_to_command_line( | |
| after_proto.actions, action_to_output_files_after | |
| ) | |
| for output_files in output_to_command_line_before: | |
| arguments = output_to_command_line_before[output_files] | |
| after_arguments = output_to_command_line_after.get(output_files, None) | |
| if after_arguments and arguments != after_arguments: | |
| _print_diff( | |
| output_files, | |
| arguments, | |
| after_arguments, | |
| "cmdline", | |
| before_file, | |
| after_file, | |
| ) | |
| found_difference = True | |
| if "inputs" in attrs: | |
| output_to_input_files_before = _map_output_files_to_input_artifacts( | |
| before_proto, artifacts_before, action_to_output_files_before | |
| ) | |
| output_to_input_files_after = _map_output_files_to_input_artifacts( | |
| after_proto, artifacts_after, action_to_output_files_after | |
| ) | |
| for output_files in output_to_input_files_before: | |
| before_inputs = output_to_input_files_before[output_files] | |
| after_inputs = output_to_input_files_after.get(output_files, None) | |
| if after_inputs and before_inputs != after_inputs: | |
| _print_diff( | |
| output_files, | |
| before_inputs, | |
| after_inputs, | |
| "inputs", | |
| before_file, | |
| after_file, | |
| ) | |
| found_difference = True | |
| if not found_difference: | |
| print("No difference") | |
| def to_absolute_path(path): | |
| path = os.path.expanduser(path) | |
| if os.path.isabs(path): | |
| return path | |
| else: | |
| if "BUILD_WORKING_DIRECTORY" in os.environ: | |
| return os.path.join(os.environ["BUILD_WORKING_DIRECTORY"], path) | |
| else: | |
| return path | |
| def _read_proto( | |
| file_path: str, input_type: str | |
| ) -> analysis_v2_pb2.ActionGraphContainer: | |
| """Reads an ActionGraphContainer proto from a file.""" | |
| if input_type == "proto": | |
| with open(file_path, "rb") as f: | |
| return analysis_v2_pb2.ActionGraphContainer.FromString(f.read()) | |
| elif input_type == "streamed_proto": | |
| result_proto = analysis_v2_pb2.ActionGraphContainer() | |
| with open(file_path, "rb") as f: | |
| while True: | |
| msg = proto.parse_length_prefixed( | |
| analysis_v2_pb2.ActionGraphContainer, f | |
| ) | |
| if msg is None: | |
| break | |
| result_proto.MergeFrom(msg) | |
| return result_proto | |
| else: | |
| with open(file_path, "r") as f: | |
| return text_format.Parse(f.read(), analysis_v2_pb2.ActionGraphContainer()) | |
| def main(unused_argv): | |
| before_file = to_absolute_path(flags.FLAGS.before) | |
| after_file = to_absolute_path(flags.FLAGS.after) | |
| input_type = flags.FLAGS.input_type | |
| attrs = flags.FLAGS.attrs | |
| max_mem_alloc_mb = flags.FLAGS.max_mem_alloc_mb | |
| # resource lib isn't available on Windows. | |
| if os.name != "nt": | |
| max_heap_bytes = max_mem_alloc_mb * 1024 * 1024 | |
| resource.setrlimit(resource.RLIMIT_AS, (max_heap_bytes, max_heap_bytes)) | |
| try: | |
| before_proto = _read_proto(before_file, input_type) | |
| after_proto = _read_proto(after_file, input_type) | |
| _aquery_diff(before_proto, after_proto, attrs, before_file, after_file) | |
| except MemoryError: | |
| print( | |
| "aquery_differ is known to cause OOM issue with large inputs. More" | |
| " details: b/154620006.", | |
| file=sys.stderr, | |
| ) | |
| print( | |
| "Max mem space of {}MB exceeded".format(max_mem_alloc_mb), | |
| file=sys.stderr, | |
| ) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| app.run(main) |