• 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
17"""Helpers pertaining to clang compile actions."""
18
19import collections
20import difflib
21import pathlib
22import subprocess
23from typing import Callable
24from commands import CommandInfo
25from commands import flag_repr
26from commands import is_flag_starts_with
27from commands import parse_flag_groups
28
29
30class ClangCompileInfo(CommandInfo):
31  """Contains information about a clang compile action commandline."""
32
33  def __init__(self, tool, args):
34    CommandInfo.__init__(self, tool, args)
35
36    flag_groups = parse_flag_groups(args, _custom_flag_group)
37
38    misc = []
39    i_includes = []
40    iquote_includes = []
41    isystem_includes = []
42    defines = []
43    warnings = []
44    file_flags = []
45    for g in flag_groups:
46      if is_flag_starts_with("D", g) or is_flag_starts_with("U", g):
47        defines += [g]
48      elif is_flag_starts_with("I", g):
49        i_includes += [g]
50      elif is_flag_starts_with("isystem", g):
51        isystem_includes += [g]
52      elif is_flag_starts_with("iquote", g):
53        iquote_includes += [g]
54      elif is_flag_starts_with("W", g) or is_flag_starts_with("w", g):
55        warnings += [g]
56      elif (is_flag_starts_with("MF", g) or is_flag_starts_with("o", g) or
57            _is_src_group(g)):
58        file_flags += [g]
59      else:
60        misc += [g]
61    self.misc_flags = sorted(misc, key=flag_repr)
62    self.i_includes = _process_includes(i_includes)
63    self.iquote_includes = _process_includes(iquote_includes)
64    self.isystem_includes = _process_includes(isystem_includes)
65    self.defines = _process_defines(defines)
66    self.warnings = warnings
67    self.file_flags = file_flags
68
69  def _str_for_field(self, field_name, values):
70    s = "  " + field_name + ":\n"
71    for x in values:
72      s += "    " + flag_repr(x) + "\n"
73    return s
74
75  def __str__(self):
76    s = "ClangCompileInfo:\n"
77    s += self._str_for_field("Includes (-I)", self.i_includes)
78    s += self._str_for_field("Includes (-iquote)", self.iquote_includes)
79    s += self._str_for_field("Includes (-isystem)", self.isystem_includes)
80    s += self._str_for_field("Defines", self.defines)
81    s += self._str_for_field("Warnings", self.warnings)
82    s += self._str_for_field("Files", self.file_flags)
83    s += self._str_for_field("Misc", self.misc_flags)
84    return s
85
86
87def _is_src_group(x):
88  """Returns true if the given flag group describes a source file."""
89  return isinstance(x, str) and x.endswith(".cpp")
90
91
92def _custom_flag_group(x):
93  """Identifies single-arg flag groups for clang compiles.
94
95  Returns a flag group if the given argument corresponds to a single-argument
96  flag group for clang compile. (For example, `-c` is a single-arg flag for
97  clang compiles, but may not be for other tools.)
98
99  See commands.parse_flag_groups documentation for signature details."""
100  if x.startswith("-I") and len(x) > 2:
101    return ("I", x[2:])
102  if x.startswith("-W") and len(x) > 2:
103    return (x)
104  elif x == "-c":
105    return x
106  return None
107
108
109def _process_defines(defs):
110  """Processes and returns deduplicated define flags from all define args."""
111  # TODO(cparsons): Determine and return effective defines (returning the last
112  # set value).
113  defines_by_var = collections.defaultdict(list)
114  for x in defs:
115    if isinstance(x, tuple):
116      var_name = x[0][2:]
117    else:
118      var_name = x[2:]
119    defines_by_var[var_name].append(x)
120  result = []
121  for k in sorted(defines_by_var):
122    d = defines_by_var[k]
123    for x in d:
124      result += [x]
125  return result
126
127
128def _process_includes(includes):
129  # Drop genfiles directories; makes diffing easier.
130  result = []
131  for x in includes:
132    if isinstance(x, tuple):
133      if not x[1].startswith("bazel-out"):
134        result += [x]
135    else:
136      result += [x]
137  return result
138
139
140# given a file, give a list of "information" about it
141ExtractInfo = Callable[[pathlib.Path], list[str]]
142
143
144def _diff(left_path: pathlib.Path, right_path: pathlib.Path, tool_name: str,
145    tool: ExtractInfo) -> list[str]:
146  """Returns a list of strings describing differences in `.o` files.
147  Returns the empty list if these files are deemed "similar enough".
148
149  The given files must exist and must be object (.o) files."""
150  errors = []
151
152  left = tool(left_path)
153  right = tool(right_path)
154  comparator = difflib.context_diff(left, right)
155  difflines = list(comparator)
156  if difflines:
157    err = "\n".join(difflines)
158    errors.append(
159      f"{left_path}\ndiffers from\n{right_path}\nper {tool_name}:\n{err}")
160  return errors
161
162
163def _external_tool(*args) -> ExtractInfo:
164  return lambda file: subprocess.run([*args, str(file)],
165                                     check=True, capture_output=True,
166                                     encoding="utf-8").stdout.splitlines()
167
168
169# TODO(usta) use nm as a data dependency
170def nm_differences(left_path: pathlib.Path, right_path: pathlib.Path) -> list[
171  str]:
172  """Returns differences in symbol tables.
173  Returns the empty list if these files are deemed "similar enough".
174
175  The given files must exist and must be object (.o) files."""
176  return _diff(left_path, right_path, "symbol tables", _external_tool("nm"))
177
178
179# TODO(usta) use readelf as a data dependency
180def elf_differences(left_path: pathlib.Path, right_path: pathlib.Path) -> list[
181  str]:
182  """Returns differences in elf headers.
183  Returns the empty list if these files are deemed "similar enough".
184
185  The given files must exist and must be object (.o) files."""
186  return _diff(left_path, right_path, "elf headers",
187               _external_tool("readelf", "-h"))
188