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