blob: ddb4cabe7148318dd73300cfaf5a3df989ed5eda [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2019 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.
#
import logging
import os
import re
import subprocess
import tempfile
from contextlib import nullcontext
log = logging.getLogger(__name__)
def _collapse_abidiff_impacted_interfaces(text):
"""Removes impacted interfaces details, leaving just the summary count."""
return re.sub(
r"^( *)([^ ]* impacted interfaces?):\n(?:^\1 .*\n)*",
r"\1\2\n",
text,
flags=re.MULTILINE)
def _collapse_abidiff_offset_changes(text):
"""Replaces "offset changed" lines with a one-line summary."""
regex = re.compile(
r"^( *)('.*') offset changed from .* to .* \(in bits\) (\(by .* bits\))$")
items = []
indent = ""
offset = ""
new_text = []
def emit_pending():
if not items:
return
count = len(items)
if count == 1:
only = items[0]
line = "{}{} offset changed {}\n".format(indent, only, offset)
else:
first = items[0]
last = items[-1]
line = "{}{} ({} .. {}) offsets changed {}\n".format(
indent, count, first, last, offset)
del items[:]
new_text.append(line)
for line in text.splitlines(True):
match = regex.match(line)
if match:
(new_indent, item, new_offset) = match.group(1, 2, 3)
if new_indent != indent or new_offset != offset:
emit_pending()
indent = new_indent
offset = new_offset
items.append(item)
else:
emit_pending()
new_text.append(line)
emit_pending()
return "".join(new_text)
def _collapse_abidiff_CRC_changes(text, limit):
"""Preserves some CRC-only changes and summarises the rest.
A CRC-only change is one like the following (indented and with a
trailing blank line).
[C] 'function void* blah(type*)' at core.c:666:1 has some sub-type changes:
CRC value (modversions) changed from 0xf0f8820e to 0xe817181d
Up to the first 'limit' changes will be emitted at the end of the
enclosing diff section. Any remaining ones will be summarised with
a line like the following.
... 17 omitted; 27 symbols have only CRC changes
Args:
text: The report text.
limit: The maximum, integral number of CRC-only changes per diff section.
Returns:
Updated report text.
"""
section_regex = re.compile(r"^[^ \n]")
change_regex = re.compile(r"^ \[C\] .*:$")
crc_regex = re.compile(r"^ CRC.*changed from [^ ]* to [^ ]*$")
blank_regex = re.compile(r"^$")
pending = []
new_lines = []
def emit_pending():
if not pending:
return
for (symbol_details, crc_details) in pending[0:limit]:
new_lines.extend([symbol_details, crc_details, "\n"])
count = len(pending)
if count > limit:
new_lines.append(" ... {} omitted; {} symbols have only CRC changes\n\n"
.format(count - limit, count))
pending.clear()
lines = text.splitlines(True)
index = 0
while index < len(lines):
line = lines[index]
if section_regex.match(line):
emit_pending()
if (index + 2 < len(lines) and change_regex.match(line) and
crc_regex.match(lines[index+1]) and blank_regex.match(lines[index+2])):
pending.append((line, lines[index+1]))
index += 3
continue
new_lines.append(line)
index += 1
emit_pending()
return "".join(new_lines)
class AbiTool(object):
"""Base class for different kinds of abi analysis tools"""
def dump_kernel_abi(self, linux_tree, dump_path, symbol_list,
vmlinux_path=None):
raise NotImplementedError()
def diff_abi(self, old_dump, new_dump, diff_report, short_report,
symbol_list, full_report):
raise NotImplementedError()
def name(self):
raise NotImplementedError()
ABIDIFF_ERROR = (1<<0)
ABIDIFF_USAGE_ERROR = (1<<1)
ABIDIFF_ABI_CHANGE = (1<<2)
ABIDIFF_ABI_INCOMPATIBLE_CHANGE = (1<<3)
class Libabigail(AbiTool):
"""Concrete AbiTool implementation for libabigail"""
def dump_kernel_abi(self, linux_tree, dump_path, symbol_list,
vmlinux_path=None):
with tempfile.NamedTemporaryFile() as temp_file:
temp_path = temp_file.name
dump_abi_cmd = ["abidw",
# omit various sources of indeterministic abidw output
"--no-corpus-path",
"--no-comp-dir-path",
# use (more) stable type ids
"--type-id-style",
"hash",
# the path containing vmlinux and *.ko
"--linux-tree",
linux_tree,
"--out-file",
temp_path]
if vmlinux_path is not None:
dump_abi_cmd.extend(["--vmlinux", vmlinux_path])
if symbol_list is not None:
dump_abi_cmd.extend(["--kmi-whitelist", symbol_list])
subprocess.check_call(dump_abi_cmd)
tidy_abi_command = ["abitidy",
"--all",
"--input", temp_path,
"--output", dump_path]
subprocess.check_call(tidy_abi_command)
def diff_abi(self, old_dump, new_dump, diff_report, short_report,
symbol_list, full_report):
log.info("libabigail diffing: {} and {} at {}".format(old_dump,
new_dump,
diff_report))
diff_abi_cmd = ["abidiff",
"--flag-indirect",
old_dump,
new_dump]
if not full_report:
diff_abi_cmd.extend([
"--leaf-changes-only",
"--impacted-interfaces",
])
if symbol_list is not None:
diff_abi_cmd.extend(["--kmi-whitelist", symbol_list])
abi_changed = False
with open(diff_report, "w") as out:
try:
subprocess.check_call(diff_abi_cmd, stdout=out, stderr=out)
except subprocess.CalledProcessError as e:
if e.returncode & (ABIDIFF_ERROR | ABIDIFF_USAGE_ERROR):
raise
abi_changed = True # actual abi change
if short_report is not None:
with open(diff_report) as full_report:
with open(short_report, "w") as out:
text = full_report.read()
text = _collapse_abidiff_impacted_interfaces(text)
text = _collapse_abidiff_offset_changes(text)
text = _collapse_abidiff_CRC_changes(text, 3)
out.write(text)
return abi_changed
class Stg(AbiTool):
DIFF_ERROR = (1<<0)
DIFF_ABI_CHANGE = (1<<2)
"""" Concrete AbiTool implementation for STG """
def dump_kernel_abi(self, linux_tree, dump_path, symbol_list,
vmlinux_path=None):
raise
def diff_abi(self, old_dump, new_dump, diff_report, short_report=None,
symbol_list=None, full_report=None):
# shoehorn the interface
basename = diff_report
dumps = [old_dump, new_dump]
# if a symbol list has been specified, we need some scratch space
if symbol_list:
context = tempfile.TemporaryDirectory()
else:
context = nullcontext()
with context as temp:
# if a symbol list has been specified, filter both input files
if symbol_list:
for ix in [0, 1]:
raw = dumps[ix]
cooked = os.path.join(temp, f"dump{ix}")
log.info(f"filtering {raw} to {cooked}")
subprocess.check_call(
["abitidy", "-S", symbol_list, "-i", raw, "-o", cooked])
dumps[ix] = cooked
log.info(f"stgdiff {dumps[0]} {dumps[1]} at {basename}.*")
command = ["stgdiff", "--abi", dumps[0], dumps[1]]
for f in ["plain", "flat", "small", "viz"]:
command.extend(["--format", f, "--output", f"{basename}.{f}"])
abi_changed = False
with open(f"{basename}.errors", "w") as out:
try:
subprocess.check_call(command, stdout=out, stderr=out)
except subprocess.CalledProcessError as e:
if e.returncode & self.DIFF_ERROR:
raise
abi_changed = True
return abi_changed
def get_abi_tool(abi_tool = "libabigail"):
log.info(f"using {abi_tool} for abi analysis")
if abi_tool == "libabigail":
return Libabigail()
if abi_tool == "STG":
return Stg()
raise ValueError("not a valid abi_tool: %s" % abi_tool)