| #!/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")) |