blob: 57238dbd14da0575755bdce2915a252169f45a58 [file] [log] [blame]
#!/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 pathlib
import subprocess
from commands import CommandInfo
from commands import flag_repr
from commands import is_flag_starts_with
from commands import parse_flag_groups
from diffs.diff import ExtractInfo
from diffs.context import ContextDiff
from diffs.nm import NmSymbolDiff
from diffs.bloaty import BloatyDiff
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 = []
features = []
libraries = []
linker_args = []
assembler_args = []
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("f", g):
features += [g]
elif is_flag_starts_with("l", g):
libraries += [g]
elif is_flag_starts_with("Wl", g):
linker_args += [g]
elif is_flag_starts_with("Wa", g) and not is_flag_starts_with("Wall", g):
assembler_args += [g]
elif is_flag_starts_with("W", g) or is_flag_starts_with("w", g):
warnings += [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("MF", g)
or is_flag_starts_with("o", g)
or _is_src_group(g)
):
file_flags += [g]
else:
misc += [g]
self.features = features
self.defines = _process_defines(defines)
self.libraries = libraries
self.linker_args = linker_args
self.assembler_args = assembler_args
self.i_includes = _process_includes(i_includes)
self.iquote_includes = _process_includes(iquote_includes)
self.isystem_includes = _process_includes(isystem_includes)
self.file_flags = file_flags
self.warnings = warnings
self.misc_flags = sorted(misc, key=flag_repr)
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"
for label, fields in {
"Features": self.features,
"Defines": self.defines,
"Libraries": self.libraries,
"Linker args": self.linker_args,
"Assembler args": self.assembler_args,
"Includes (-I,": self.i_includes,
"Includes (-iquote,": self.iquote_includes,
"Includes (-isystem,": self.isystem_includes,
"Files": self.file_flags,
"Warnings": self.warnings,
"Misc": self.misc_flags,
}.items():
if len(fields) > 0:
s += self._str_for_field(label, list(set(fields)))
return s
def compare(self, other):
"""computes difference in arguments from another ClangCompileInfo"""
diffs = ClangCompileInfo(self.tool, [])
diffs.defines = [i for i in self.defines if i not in other.defines]
diffs.warnings = [i for i in self.warnings if i not in other.warnings]
diffs.features = [i for i in self.features if i not in other.features]
diffs.libraries = [i for i in self.libraries if i not in other.libraries]
diffs.linker_args = [
i for i in self.linker_args if i not in other.linker_args
]
diffs.assembler_args = [
i for i in self.assembler_args if i not in other.assembler_args
]
diffs.i_includes = [i for i in self.i_includes if i not in other.i_includes]
diffs.iquote_includes = [
i for i in self.iquote_includes if i not in other.iquote_includes
]
diffs.isystem_includes = [
i for i in self.isystem_includes if i not in other.isystem_includes
]
diffs.file_flags = [i for i in self.file_flags if i not in other.file_flags]
diffs.misc_flags = [i for i in self.misc_flags if i not in other.misc_flags]
return diffs
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
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".
"""
return NmSymbolDiff(_external_tool("nm"), "symbol tables").diff(
left_path, right_path
)
# 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 ContextDiff(_external_tool("readelf", "-h"), "elf headers").diff(
left_path, right_path
)
# TODO(usta) use bloaty as a data dependency
def bloaty_differences(
left_path: pathlib.Path, right_path: pathlib.Path
) -> list[str]:
"""Returns differences in symbol and section tables.
Returns the empty list if these files are deemed "similar enough".
The given files must exist and must be object (.o) files.
"""
return _bloaty_differences(left_path, right_path)
# TODO(usta) use bloaty as a data dependency
def bloaty_differences_compileunits(
left_path: pathlib.Path, right_path: pathlib.Path
) -> list[str]:
"""Returns differences in symbol and section tables.
Returns the empty list if these files are deemed "similar enough".
The given files must exist and must be object (.o) files.
"""
return _bloaty_differences(left_path, right_path, True)
# TODO(usta) use bloaty as a data dependency
def _bloaty_differences(
left_path: pathlib.Path, right_path: pathlib.Path, debug=False
) -> list[str]:
symbols = BloatyDiff(
"symbol tables", "symbols", has_debug_symbols=debug
).diff(left_path, right_path)
sections = BloatyDiff(
"section tables", "sections", has_debug_symbols=debug
).diff(left_path, right_path)
segments = BloatyDiff(
"segment tables", "segments", has_debug_symbols=debug
).diff(left_path, right_path)
return symbols + sections + segments