• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#   http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License."""
16"""Helpers pertaining to clang compile actions."""
17
18import collections
19import pathlib
20import subprocess
21from commands import CommandInfo
22from commands import flag_repr
23from commands import is_flag_starts_with
24from commands import parse_flag_groups
25from diffs.diff import ExtractInfo
26from diffs.context import ContextDiff
27from diffs.nm import NmSymbolDiff
28from diffs.bloaty import BloatyDiff
29
30
31class ClangCompileInfo(CommandInfo):
32  """Contains information about a clang compile action commandline."""
33
34  def __init__(self, tool, args):
35    CommandInfo.__init__(self, tool, args)
36
37    flag_groups = parse_flag_groups(args, _custom_flag_group)
38
39    misc = []
40    i_includes = []
41    iquote_includes = []
42    isystem_includes = []
43    defines = []
44    warnings = []
45    file_flags = []
46    for g in flag_groups:
47      if is_flag_starts_with("D", g) or is_flag_starts_with("U", g):
48        defines += [g]
49      elif is_flag_starts_with("I", g):
50        i_includes += [g]
51      elif is_flag_starts_with("isystem", g):
52        isystem_includes += [g]
53      elif is_flag_starts_with("iquote", g):
54        iquote_includes += [g]
55      elif is_flag_starts_with("W", g) or is_flag_starts_with("w", g):
56        warnings += [g]
57      elif (is_flag_starts_with("MF", g) or is_flag_starts_with("o", g) or
58            _is_src_group(g)):
59        file_flags += [g]
60      else:
61        misc += [g]
62    self.misc_flags = sorted(misc, key=flag_repr)
63    self.i_includes = _process_includes(i_includes)
64    self.iquote_includes = _process_includes(iquote_includes)
65    self.isystem_includes = _process_includes(isystem_includes)
66    self.defines = _process_defines(defines)
67    self.warnings = warnings
68    self.file_flags = file_flags
69
70  def _str_for_field(self, field_name, values):
71    s = "  " + field_name + ":\n"
72    for x in values:
73      s += "    " + flag_repr(x) + "\n"
74    return s
75
76  def __str__(self):
77    s = "ClangCompileInfo:\n"
78    s += self._str_for_field("Includes (-I)", self.i_includes)
79    s += self._str_for_field("Includes (-iquote)", self.iquote_includes)
80    s += self._str_for_field("Includes (-isystem)", self.isystem_includes)
81    s += self._str_for_field("Defines", self.defines)
82    s += self._str_for_field("Warnings", self.warnings)
83    s += self._str_for_field("Files", self.file_flags)
84    s += self._str_for_field("Misc", self.misc_flags)
85    return s
86
87  def compare(self, other):
88    """computes difference in arguments from another ClangCompileInfo"""
89    diffs = ClangCompileInfo(self.tool, [])
90    diffs.i_includes = [i for i in self.i_includes if i not in other.i_includes]
91    diffs.iquote_includes = [
92        i for i in self.iquote_includes if i not in other.iquote_includes
93    ]
94    diffs.isystem_includes = [
95        i for i in self.isystem_includes if i not in other.isystem_includes
96    ]
97    diffs.defines = [i for i in self.defines if i not in other.defines]
98    diffs.warnings = [i for i in self.warnings if i not in other.warnings]
99    diffs.file_flags = [i for i in self.file_flags if i not in other.file_flags]
100    diffs.misc_flags = [i for i in self.misc_flags if i not in other.misc_flags]
101    return diffs
102
103
104def _is_src_group(x):
105  """Returns true if the given flag group describes a source file."""
106  return isinstance(x, str) and x.endswith(".cpp")
107
108
109def _custom_flag_group(x):
110  """Identifies single-arg flag groups for clang compiles.
111
112  Returns a flag group if the given argument corresponds to a single-argument
113  flag group for clang compile. (For example, `-c` is a single-arg flag for
114  clang compiles, but may not be for other tools.)
115
116  See commands.parse_flag_groups documentation for signature details.
117  """
118  if x.startswith("-I") and len(x) > 2:
119    return ("I", x[2:])
120  if x.startswith("-W") and len(x) > 2:
121    return (x)
122  elif x == "-c":
123    return x
124  return None
125
126
127def _process_defines(defs):
128  """Processes and returns deduplicated define flags from all define args."""
129  # TODO(cparsons): Determine and return effective defines (returning the last
130  # set value).
131  defines_by_var = collections.defaultdict(list)
132  for x in defs:
133    if isinstance(x, tuple):
134      var_name = x[0][2:]
135    else:
136      var_name = x[2:]
137    defines_by_var[var_name].append(x)
138  result = []
139  for k in sorted(defines_by_var):
140    d = defines_by_var[k]
141    for x in d:
142      result += [x]
143  return result
144
145
146def _process_includes(includes):
147  # Drop genfiles directories; makes diffing easier.
148  result = []
149  for x in includes:
150    if isinstance(x, tuple):
151      if not x[1].startswith("bazel-out"):
152        result += [x]
153    else:
154      result += [x]
155  return result
156
157
158def _external_tool(*args) -> ExtractInfo:
159  return lambda file: subprocess.run(
160      [*args, str(file)
161      ], check=True, capture_output=True, encoding="utf-8").stdout.splitlines()
162
163
164# TODO(usta) use nm as a data dependency
165def nm_differences(left_path: pathlib.Path,
166                   right_path: pathlib.Path) -> list[str]:
167  """Returns differences in symbol tables.
168
169  Returns the empty list if these files are deemed "similar enough".
170  """
171  return NmSymbolDiff(_external_tool("nm"),
172                      "symbol tables").diff(left_path, right_path)
173
174
175# TODO(usta) use readelf as a data dependency
176def elf_differences(left_path: pathlib.Path,
177                    right_path: pathlib.Path) -> list[str]:
178  """Returns differences in elf headers.
179
180  Returns the empty list if these files are deemed "similar enough".
181
182  The given files must exist and must be object (.o) files.
183  """
184  return ContextDiff(_external_tool("readelf", "-h"),
185                     "elf headers").diff(left_path, right_path)
186
187
188# TODO(usta) use bloaty as a data dependency
189def bloaty_differences(left_path: pathlib.Path,
190                       right_path: pathlib.Path) -> list[str]:
191  """Returns differences in symbol and section tables.
192
193  Returns the empty list if these files are deemed "similar enough".
194
195  The given files must exist and must be object (.o) files.
196  """
197  return _bloaty_differences(left_path, right_path)
198
199
200# TODO(usta) use bloaty as a data dependency
201def bloaty_differences_compileunits(left_path: pathlib.Path,
202                                    right_path: pathlib.Path) -> list[str]:
203  """Returns differences in symbol and section tables.
204
205  Returns the empty list if these files are deemed "similar enough".
206
207  The given files must exist and must be object (.o) files.
208  """
209  return _bloaty_differences(left_path, right_path, True)
210
211
212# TODO(usta) use bloaty as a data dependency
213def _bloaty_differences(left_path: pathlib.Path,
214                        right_path: pathlib.Path,
215                        debug=False) -> list[str]:
216  symbols = BloatyDiff(
217      "symbol tables", "symbols",
218      has_debug_symbols=debug).diff(left_path, right_path)
219  sections = BloatyDiff(
220      "section tables", "sections",
221      has_debug_symbols=debug).diff(left_path, right_path)
222  segments = BloatyDiff(
223      "segment tables", "segments",
224      has_debug_symbols=debug).diff(left_path, right_path)
225  return symbols + sections + segments
226