| #!/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 pathlib |
| import subprocess |
| from commands import CommandInfo |
| from commands import flag_repr |
| from commands import is_flag_starts_with |
| from commands import parse_flag_groups |
| from diffs.diff import ExtractInfo |
| from diffs.context import ContextDiff |
| from diffs.nm import NmSymbolDiff |
| from diffs.bloaty import BloatyDiff |
| |
| |
| 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 = [] |
| features = [] |
| libraries = [] |
| linker_args = [] |
| assembler_args = [] |
| 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("f", g): |
| features += [g] |
| elif is_flag_starts_with("l", g): |
| libraries += [g] |
| elif is_flag_starts_with("Wl", g): |
| linker_args += [g] |
| elif is_flag_starts_with("Wa", g) and not is_flag_starts_with("Wall", g): |
| assembler_args += [g] |
| elif is_flag_starts_with("W", g) or is_flag_starts_with("w", g): |
| warnings += [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("MF", g) |
| or is_flag_starts_with("o", g) |
| or _is_src_group(g) |
| ): |
| file_flags += [g] |
| else: |
| misc += [g] |
| self.features = features |
| self.defines = _process_defines(defines) |
| self.libraries = libraries |
| self.linker_args = linker_args |
| self.assembler_args = assembler_args |
| self.i_includes = _process_includes(i_includes) |
| self.iquote_includes = _process_includes(iquote_includes) |
| self.isystem_includes = _process_includes(isystem_includes) |
| self.file_flags = file_flags |
| self.warnings = warnings |
| self.misc_flags = sorted(misc, key=flag_repr) |
| |
| 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" |
| |
| for label, fields in { |
| "Features": self.features, |
| "Defines": self.defines, |
| "Libraries": self.libraries, |
| "Linker args": self.linker_args, |
| "Assembler args": self.assembler_args, |
| "Includes (-I,": self.i_includes, |
| "Includes (-iquote,": self.iquote_includes, |
| "Includes (-isystem,": self.isystem_includes, |
| "Files": self.file_flags, |
| "Warnings": self.warnings, |
| "Misc": self.misc_flags, |
| }.items(): |
| if len(fields) > 0: |
| s += self._str_for_field(label, list(set(fields))) |
| |
| return s |
| |
| def compare(self, other): |
| """computes difference in arguments from another ClangCompileInfo""" |
| diffs = ClangCompileInfo(self.tool, []) |
| diffs.defines = [i for i in self.defines if i not in other.defines] |
| diffs.warnings = [i for i in self.warnings if i not in other.warnings] |
| diffs.features = [i for i in self.features if i not in other.features] |
| diffs.libraries = [i for i in self.libraries if i not in other.libraries] |
| diffs.linker_args = [ |
| i for i in self.linker_args if i not in other.linker_args |
| ] |
| diffs.assembler_args = [ |
| i for i in self.assembler_args if i not in other.assembler_args |
| ] |
| diffs.i_includes = [i for i in self.i_includes if i not in other.i_includes] |
| diffs.iquote_includes = [ |
| i for i in self.iquote_includes if i not in other.iquote_includes |
| ] |
| diffs.isystem_includes = [ |
| i for i in self.isystem_includes if i not in other.isystem_includes |
| ] |
| diffs.file_flags = [i for i in self.file_flags if i not in other.file_flags] |
| diffs.misc_flags = [i for i in self.misc_flags if i not in other.misc_flags] |
| return diffs |
| |
| |
| 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 |
| |
| |
| 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". |
| """ |
| return NmSymbolDiff(_external_tool("nm"), "symbol tables").diff( |
| left_path, right_path |
| ) |
| |
| |
| # 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 ContextDiff(_external_tool("readelf", "-h"), "elf headers").diff( |
| left_path, right_path |
| ) |
| |
| |
| # TODO(usta) use bloaty as a data dependency |
| def bloaty_differences( |
| left_path: pathlib.Path, right_path: pathlib.Path |
| ) -> list[str]: |
| """Returns differences in symbol and section tables. |
| |
| Returns the empty list if these files are deemed "similar enough". |
| |
| The given files must exist and must be object (.o) files. |
| """ |
| return _bloaty_differences(left_path, right_path) |
| |
| |
| # TODO(usta) use bloaty as a data dependency |
| def bloaty_differences_compileunits( |
| left_path: pathlib.Path, right_path: pathlib.Path |
| ) -> list[str]: |
| """Returns differences in symbol and section tables. |
| |
| Returns the empty list if these files are deemed "similar enough". |
| |
| The given files must exist and must be object (.o) files. |
| """ |
| return _bloaty_differences(left_path, right_path, True) |
| |
| |
| # TODO(usta) use bloaty as a data dependency |
| def _bloaty_differences( |
| left_path: pathlib.Path, right_path: pathlib.Path, debug=False |
| ) -> list[str]: |
| symbols = BloatyDiff( |
| "symbol tables", "symbols", has_debug_symbols=debug |
| ).diff(left_path, right_path) |
| sections = BloatyDiff( |
| "section tables", "sections", has_debug_symbols=debug |
| ).diff(left_path, right_path) |
| segments = BloatyDiff( |
| "segment tables", "segments", has_debug_symbols=debug |
| ).diff(left_path, right_path) |
| return symbols + sections + segments |