Merge remote-tracking branch 'upstream-public/pr/1433' into development
diff --git a/scripts/abi_check.py b/scripts/abi_check.py
new file mode 100755
index 0000000..8f9cd0f
--- /dev/null
+++ b/scripts/abi_check.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+"""
+This file is part of Mbed TLS (https://tls.mbed.org)
+
+Copyright (c) 2018, Arm Limited, All Rights Reserved
+
+Purpose
+
+This script is a small wrapper around the abi-compliance-checker and
+abi-dumper tools, applying them to compare the ABI and API of the library
+files from two different Git revisions within an Mbed TLS repository.
+The results of the comparison are formatted as HTML and stored at
+a configurable location. Returns 0 on success, 1 on ABI/API non-compliance,
+and 2 if there is an error while running the script.
+Note: must be run from Mbed TLS root.
+"""
+
+import os
+import sys
+import traceback
+import shutil
+import subprocess
+import argparse
+import logging
+import tempfile
+
+
+class AbiChecker(object):
+
+ def __init__(self, report_dir, old_rev, new_rev, keep_all_reports):
+ self.repo_path = "."
+ self.log = None
+ self.setup_logger()
+ self.report_dir = os.path.abspath(report_dir)
+ self.keep_all_reports = keep_all_reports
+ self.should_keep_report_dir = os.path.isdir(self.report_dir)
+ self.old_rev = old_rev
+ self.new_rev = new_rev
+ self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
+ self.old_dumps = {}
+ self.new_dumps = {}
+ self.git_command = "git"
+ self.make_command = "make"
+
+ def check_repo_path(self):
+ current_dir = os.path.realpath('.')
+ root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+ if current_dir != root_dir:
+ raise Exception("Must be run from Mbed TLS root")
+
+ def setup_logger(self):
+ self.log = logging.getLogger()
+ self.log.setLevel(logging.INFO)
+ self.log.addHandler(logging.StreamHandler())
+
+ def check_abi_tools_are_installed(self):
+ for command in ["abi-dumper", "abi-compliance-checker"]:
+ if not shutil.which(command):
+ raise Exception("{} not installed, aborting".format(command))
+
+ def get_clean_worktree_for_git_revision(self, git_rev):
+ self.log.info(
+ "Checking out git worktree for revision {}".format(git_rev)
+ )
+ git_worktree_path = tempfile.mkdtemp()
+ worktree_process = subprocess.Popen(
+ [self.git_command, "worktree", "add", git_worktree_path, git_rev],
+ cwd=self.repo_path,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ worktree_output, _ = worktree_process.communicate()
+ self.log.info(worktree_output.decode("utf-8"))
+ if worktree_process.returncode != 0:
+ raise Exception("Checking out worktree failed, aborting")
+ return git_worktree_path
+
+ def build_shared_libraries(self, git_worktree_path):
+ my_environment = os.environ.copy()
+ my_environment["CFLAGS"] = "-g -Og"
+ my_environment["SHARED"] = "1"
+ make_process = subprocess.Popen(
+ self.make_command,
+ env=my_environment,
+ cwd=git_worktree_path,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ make_output, _ = make_process.communicate()
+ self.log.info(make_output.decode("utf-8"))
+ if make_process.returncode != 0:
+ raise Exception("make failed, aborting")
+
+ def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
+ abi_dumps = {}
+ for mbed_module in self.mbedtls_modules:
+ output_path = os.path.join(
+ self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
+ )
+ abi_dump_command = [
+ "abi-dumper",
+ os.path.join(
+ git_worktree_path, "library", mbed_module + ".so"),
+ "-o", output_path,
+ "-lver", git_ref
+ ]
+ abi_dump_process = subprocess.Popen(
+ abi_dump_command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ abi_dump_output, _ = abi_dump_process.communicate()
+ self.log.info(abi_dump_output.decode("utf-8"))
+ if abi_dump_process.returncode != 0:
+ raise Exception("abi-dumper failed, aborting")
+ abi_dumps[mbed_module] = output_path
+ return abi_dumps
+
+ def cleanup_worktree(self, git_worktree_path):
+ shutil.rmtree(git_worktree_path)
+ worktree_process = subprocess.Popen(
+ [self.git_command, "worktree", "prune"],
+ cwd=self.repo_path,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ worktree_output, _ = worktree_process.communicate()
+ self.log.info(worktree_output.decode("utf-8"))
+ if worktree_process.returncode != 0:
+ raise Exception("Worktree cleanup failed, aborting")
+
+ def get_abi_dump_for_ref(self, git_rev):
+ git_worktree_path = self.get_clean_worktree_for_git_revision(git_rev)
+ self.build_shared_libraries(git_worktree_path)
+ abi_dumps = self.get_abi_dumps_from_shared_libraries(
+ git_rev, git_worktree_path
+ )
+ self.cleanup_worktree(git_worktree_path)
+ return abi_dumps
+
+ def get_abi_compatibility_report(self):
+ compatibility_report = ""
+ compliance_return_code = 0
+ for mbed_module in self.mbedtls_modules:
+ output_path = os.path.join(
+ self.report_dir, "{}-{}-{}.html".format(
+ mbed_module, self.old_rev, self.new_rev
+ )
+ )
+ abi_compliance_command = [
+ "abi-compliance-checker",
+ "-l", mbed_module,
+ "-old", self.old_dumps[mbed_module],
+ "-new", self.new_dumps[mbed_module],
+ "-strict",
+ "-report-path", output_path
+ ]
+ abi_compliance_process = subprocess.Popen(
+ abi_compliance_command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ abi_compliance_output, _ = abi_compliance_process.communicate()
+ self.log.info(abi_compliance_output.decode("utf-8"))
+ if abi_compliance_process.returncode == 0:
+ compatibility_report += (
+ "No compatibility issues for {}\n".format(mbed_module)
+ )
+ if not self.keep_all_reports:
+ os.remove(output_path)
+ elif abi_compliance_process.returncode == 1:
+ compliance_return_code = 1
+ self.should_keep_report_dir = True
+ compatibility_report += (
+ "Compatibility issues found for {}, "
+ "for details see {}\n".format(mbed_module, output_path)
+ )
+ else:
+ raise Exception(
+ "abi-compliance-checker failed with a return code of {},"
+ " aborting".format(abi_compliance_process.returncode)
+ )
+ os.remove(self.old_dumps[mbed_module])
+ os.remove(self.new_dumps[mbed_module])
+ if not self.should_keep_report_dir and not self.keep_all_reports:
+ os.rmdir(self.report_dir)
+ self.log.info(compatibility_report)
+ return compliance_return_code
+
+ def check_for_abi_changes(self):
+ self.check_repo_path()
+ self.check_abi_tools_are_installed()
+ self.old_dumps = self.get_abi_dump_for_ref(self.old_rev)
+ self.new_dumps = self.get_abi_dump_for_ref(self.new_rev)
+ return self.get_abi_compatibility_report()
+
+
+def run_main():
+ try:
+ parser = argparse.ArgumentParser(
+ description=(
+ """This script is a small wrapper around the
+ abi-compliance-checker and abi-dumper tools, applying them
+ to compare the ABI and API of the library files from two
+ different Git revisions within an Mbed TLS repository.
+ The results of the comparison are formatted as HTML and stored
+ at a configurable location. Returns 0 on success, 1 on ABI/API
+ non-compliance, and 2 if there is an error while running the
+ script. Note: must be run from Mbed TLS root."""
+ )
+ )
+ parser.add_argument(
+ "-r", "--report-dir", type=str, default="reports",
+ help="directory where reports are stored, default is reports",
+ )
+ parser.add_argument(
+ "-k", "--keep-all-reports", action="store_true",
+ help="keep all reports, even if there are no compatibility issues",
+ )
+ parser.add_argument(
+ "-o", "--old-rev", type=str, help="revision for old version",
+ required=True
+ )
+ parser.add_argument(
+ "-n", "--new-rev", type=str, help="revision for new version",
+ required=True
+ )
+ abi_args = parser.parse_args()
+ abi_check = AbiChecker(
+ abi_args.report_dir, abi_args.old_rev,
+ abi_args.new_rev, abi_args.keep_all_reports
+ )
+ return_code = abi_check.check_for_abi_changes()
+ sys.exit(return_code)
+ except Exception:
+ traceback.print_exc()
+ sys.exit(2)
+
+
+if __name__ == "__main__":
+ run_main()