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