#!/usr/bin/env python3 # # Copyright (C) 2022 The Android Open Source Project # # 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.""" """Helpers pertaining to clang compile actions.""" import collections import difflib import pathlib import subprocess from typing import Callable from commands import CommandInfo from commands import flag_repr from commands import is_flag_starts_with from commands import parse_flag_groups class ClangCompileInfo(CommandInfo): """Contains information about a clang compile action commandline.""" def __init__(self, tool, args): CommandInfo.__init__(self, tool, args) flag_groups = parse_flag_groups(args, _custom_flag_group) misc = [] i_includes = [] iquote_includes = [] isystem_includes = [] defines = [] warnings = [] file_flags = [] for g in flag_groups: if is_flag_starts_with("D", g) or is_flag_starts_with("U", g): defines += [g] elif is_flag_starts_with("I", g): i_includes += [g] elif is_flag_starts_with("isystem", g): isystem_includes += [g] elif is_flag_starts_with("iquote", g): iquote_includes += [g] elif is_flag_starts_with("W", g) or is_flag_starts_with("w", g): warnings += [g] elif (is_flag_starts_with("MF", g) or is_flag_starts_with("o", g) or _is_src_group(g)): file_flags += [g] else: misc += [g] self.misc_flags = sorted(misc, key=flag_repr) self.i_includes = _process_includes(i_includes) self.iquote_includes = _process_includes(iquote_includes) self.isystem_includes = _process_includes(isystem_includes) self.defines = _process_defines(defines) self.warnings = warnings self.file_flags = file_flags def _str_for_field(self, field_name, values): s = " " + field_name + ":\n" for x in values: s += " " + flag_repr(x) + "\n" return s def __str__(self): s = "ClangCompileInfo:\n" s += self._str_for_field("Includes (-I)", self.i_includes) s += self._str_for_field("Includes (-iquote)", self.iquote_includes) s += self._str_for_field("Includes (-isystem)", self.isystem_includes) s += self._str_for_field("Defines", self.defines) s += self._str_for_field("Warnings", self.warnings) s += self._str_for_field("Files", self.file_flags) s += self._str_for_field("Misc", self.misc_flags) return s def _is_src_group(x): """Returns true if the given flag group describes a source file.""" return isinstance(x, str) and x.endswith(".cpp") def _custom_flag_group(x): """Identifies single-arg flag groups for clang compiles. Returns a flag group if the given argument corresponds to a single-argument flag group for clang compile. (For example, `-c` is a single-arg flag for clang compiles, but may not be for other tools.) See commands.parse_flag_groups documentation for signature details.""" if x.startswith("-I") and len(x) > 2: return ("I", x[2:]) if x.startswith("-W") and len(x) > 2: return (x) elif x == "-c": return x return None def _process_defines(defs): """Processes and returns deduplicated define flags from all define args.""" # TODO(cparsons): Determine and return effective defines (returning the last # set value). defines_by_var = collections.defaultdict(list) for x in defs: if isinstance(x, tuple): var_name = x[0][2:] else: var_name = x[2:] defines_by_var[var_name].append(x) result = [] for k in sorted(defines_by_var): d = defines_by_var[k] for x in d: result += [x] return result def _process_includes(includes): # Drop genfiles directories; makes diffing easier. result = [] for x in includes: if isinstance(x, tuple): if not x[1].startswith("bazel-out"): result += [x] else: result += [x] return result # given a file, give a list of "information" about it ExtractInfo = Callable[[pathlib.Path], list[str]] def _diff(left_path: pathlib.Path, right_path: pathlib.Path, tool_name: str, tool: ExtractInfo) -> list[str]: """Returns a list of strings describing differences in `.o` files. Returns the empty list if these files are deemed "similar enough". The given files must exist and must be object (.o) files.""" errors = [] left = tool(left_path) right = tool(right_path) comparator = difflib.context_diff(left, right) difflines = list(comparator) if difflines: err = "\n".join(difflines) errors.append( f"{left_path}\ndiffers from\n{right_path}\nper {tool_name}:\n{err}") return errors def _external_tool(*args) -> ExtractInfo: return lambda file: subprocess.run([*args, str(file)], check=True, capture_output=True, encoding="utf-8").stdout.splitlines() # TODO(usta) use nm as a data dependency def nm_differences(left_path: pathlib.Path, right_path: pathlib.Path) -> list[ str]: """Returns differences in symbol tables. Returns the empty list if these files are deemed "similar enough". The given files must exist and must be object (.o) files.""" return _diff(left_path, right_path, "symbol tables", _external_tool("nm")) # TODO(usta) use readelf as a data dependency def elf_differences(left_path: pathlib.Path, right_path: pathlib.Path) -> list[ str]: """Returns differences in elf headers. Returns the empty list if these files are deemed "similar enough". The given files must exist and must be object (.o) files.""" return _diff(left_path, right_path, "elf headers", _external_tool("readelf", "-h"))