blob: d458d56e31473d32b5437e6f9b59f3dbf544fb1e [file] [log] [blame]
Darryl Green7c2dd582018-03-01 14:53:49 +00001#!/usr/bin/env python3
Darryl Green78696802018-04-06 11:23:22 +01002"""
3This file is part of Mbed TLS (https://tls.mbed.org)
4
5Copyright (c) 2018, Arm Limited, All Rights Reserved
6
7Purpose
8
9This script is a small wrapper around the abi-compliance-checker and
10abi-dumper tools, applying them to compare the ABI and API of the library
11files from two different Git revisions within an Mbed TLS repository.
12The results of the comparison are formatted as HTML and stored at
13a configurable location. Returns 0 on success, 1 on ABI/API non-compliance,
14and 2 if there is an error while running the script.
Darryl Green418527b2018-04-16 12:02:29 +010015Note: must be run from Mbed TLS root.
Darryl Green78696802018-04-06 11:23:22 +010016"""
Darryl Green7c2dd582018-03-01 14:53:49 +000017
18import os
19import sys
20import traceback
21import shutil
22import subprocess
23import argparse
24import logging
25import tempfile
26
27
28class AbiChecker(object):
Gilles Peskine9df17632019-02-25 20:36:52 +010029 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000030
31 def __init__(self, report_dir, old_rev, new_rev, keep_all_reports):
Gilles Peskine9df17632019-02-25 20:36:52 +010032 """Instantiate the API/ABI checker.
33
34 report_dir: directory for output files
35 old_rev: reference git revision to compare against
36 new_rev: git revision to check
37 keep_all_reports: if false, delete old reports
38 """
Darryl Green7c2dd582018-03-01 14:53:49 +000039 self.repo_path = "."
40 self.log = None
41 self.setup_logger()
42 self.report_dir = os.path.abspath(report_dir)
43 self.keep_all_reports = keep_all_reports
44 self.should_keep_report_dir = os.path.isdir(self.report_dir)
45 self.old_rev = old_rev
46 self.new_rev = new_rev
47 self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
48 self.old_dumps = {}
49 self.new_dumps = {}
50 self.git_command = "git"
51 self.make_command = "make"
52
Gilles Peskine9df17632019-02-25 20:36:52 +010053 @staticmethod
54 def check_repo_path():
Darryl Greena6f430f2018-03-15 10:12:06 +000055 current_dir = os.path.realpath('.')
56 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
57 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000058 raise Exception("Must be run from Mbed TLS root")
59
60 def setup_logger(self):
61 self.log = logging.getLogger()
62 self.log.setLevel(logging.INFO)
63 self.log.addHandler(logging.StreamHandler())
64
Gilles Peskine9df17632019-02-25 20:36:52 +010065 @staticmethod
66 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +000067 for command in ["abi-dumper", "abi-compliance-checker"]:
68 if not shutil.which(command):
69 raise Exception("{} not installed, aborting".format(command))
70
71 def get_clean_worktree_for_git_revision(self, git_rev):
Gilles Peskine9df17632019-02-25 20:36:52 +010072 """Make a separate worktree with git_rev checked out.
73 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +000074 self.log.info(
75 "Checking out git worktree for revision {}".format(git_rev)
76 )
77 git_worktree_path = tempfile.mkdtemp()
78 worktree_process = subprocess.Popen(
Jaeden Amero9ef60282018-11-02 16:22:37 +000079 [self.git_command, "worktree", "add", "--detach", git_worktree_path, git_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +000080 cwd=self.repo_path,
81 stdout=subprocess.PIPE,
82 stderr=subprocess.STDOUT
83 )
84 worktree_output, _ = worktree_process.communicate()
85 self.log.info(worktree_output.decode("utf-8"))
86 if worktree_process.returncode != 0:
87 raise Exception("Checking out worktree failed, aborting")
88 return git_worktree_path
89
90 def build_shared_libraries(self, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +010091 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +000092 my_environment = os.environ.copy()
93 my_environment["CFLAGS"] = "-g -Og"
94 my_environment["SHARED"] = "1"
95 make_process = subprocess.Popen(
96 self.make_command,
97 env=my_environment,
98 cwd=git_worktree_path,
99 stdout=subprocess.PIPE,
100 stderr=subprocess.STDOUT
101 )
102 make_output, _ = make_process.communicate()
103 self.log.info(make_output.decode("utf-8"))
104 if make_process.returncode != 0:
105 raise Exception("make failed, aborting")
106
107 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +0100108 """Generate the ABI dumps for the specified git revision.
109 It must be checked out in git_worktree_path and the shared libraries
110 must have been built."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000111 abi_dumps = {}
112 for mbed_module in self.mbedtls_modules:
113 output_path = os.path.join(
114 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
115 )
116 abi_dump_command = [
117 "abi-dumper",
118 os.path.join(
119 git_worktree_path, "library", mbed_module + ".so"),
120 "-o", output_path,
121 "-lver", git_ref
122 ]
123 abi_dump_process = subprocess.Popen(
124 abi_dump_command,
125 stdout=subprocess.PIPE,
126 stderr=subprocess.STDOUT
127 )
128 abi_dump_output, _ = abi_dump_process.communicate()
129 self.log.info(abi_dump_output.decode("utf-8"))
130 if abi_dump_process.returncode != 0:
131 raise Exception("abi-dumper failed, aborting")
132 abi_dumps[mbed_module] = output_path
133 return abi_dumps
134
135 def cleanup_worktree(self, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +0100136 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000137 shutil.rmtree(git_worktree_path)
138 worktree_process = subprocess.Popen(
139 [self.git_command, "worktree", "prune"],
140 cwd=self.repo_path,
141 stdout=subprocess.PIPE,
142 stderr=subprocess.STDOUT
143 )
144 worktree_output, _ = worktree_process.communicate()
145 self.log.info(worktree_output.decode("utf-8"))
146 if worktree_process.returncode != 0:
147 raise Exception("Worktree cleanup failed, aborting")
148
149 def get_abi_dump_for_ref(self, git_rev):
Gilles Peskine9df17632019-02-25 20:36:52 +0100150 """Generate the ABI dumps for the specified git revision."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000151 git_worktree_path = self.get_clean_worktree_for_git_revision(git_rev)
152 self.build_shared_libraries(git_worktree_path)
153 abi_dumps = self.get_abi_dumps_from_shared_libraries(
154 git_rev, git_worktree_path
155 )
156 self.cleanup_worktree(git_worktree_path)
157 return abi_dumps
158
159 def get_abi_compatibility_report(self):
Gilles Peskine9df17632019-02-25 20:36:52 +0100160 """Generate a report of the differences between the reference ABI
161 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
162 be available."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000163 compatibility_report = ""
164 compliance_return_code = 0
165 for mbed_module in self.mbedtls_modules:
166 output_path = os.path.join(
167 self.report_dir, "{}-{}-{}.html".format(
168 mbed_module, self.old_rev, self.new_rev
169 )
170 )
171 abi_compliance_command = [
172 "abi-compliance-checker",
173 "-l", mbed_module,
174 "-old", self.old_dumps[mbed_module],
175 "-new", self.new_dumps[mbed_module],
176 "-strict",
177 "-report-path", output_path
178 ]
179 abi_compliance_process = subprocess.Popen(
180 abi_compliance_command,
181 stdout=subprocess.PIPE,
182 stderr=subprocess.STDOUT
183 )
184 abi_compliance_output, _ = abi_compliance_process.communicate()
185 self.log.info(abi_compliance_output.decode("utf-8"))
186 if abi_compliance_process.returncode == 0:
187 compatibility_report += (
188 "No compatibility issues for {}\n".format(mbed_module)
189 )
190 if not self.keep_all_reports:
191 os.remove(output_path)
192 elif abi_compliance_process.returncode == 1:
193 compliance_return_code = 1
194 self.should_keep_report_dir = True
195 compatibility_report += (
196 "Compatibility issues found for {}, "
197 "for details see {}\n".format(mbed_module, output_path)
198 )
199 else:
200 raise Exception(
201 "abi-compliance-checker failed with a return code of {},"
202 " aborting".format(abi_compliance_process.returncode)
203 )
204 os.remove(self.old_dumps[mbed_module])
205 os.remove(self.new_dumps[mbed_module])
206 if not self.should_keep_report_dir and not self.keep_all_reports:
207 os.rmdir(self.report_dir)
208 self.log.info(compatibility_report)
209 return compliance_return_code
210
211 def check_for_abi_changes(self):
Gilles Peskine9df17632019-02-25 20:36:52 +0100212 """Generate a report of ABI differences
213 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000214 self.check_repo_path()
215 self.check_abi_tools_are_installed()
216 self.old_dumps = self.get_abi_dump_for_ref(self.old_rev)
217 self.new_dumps = self.get_abi_dump_for_ref(self.new_rev)
218 return self.get_abi_compatibility_report()
219
220
221def run_main():
222 try:
223 parser = argparse.ArgumentParser(
224 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100225 """This script is a small wrapper around the
226 abi-compliance-checker and abi-dumper tools, applying them
227 to compare the ABI and API of the library files from two
228 different Git revisions within an Mbed TLS repository.
229 The results of the comparison are formatted as HTML and stored
230 at a configurable location. Returns 0 on success, 1 on ABI/API
231 non-compliance, and 2 if there is an error while running the
232 script. Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000233 )
234 )
235 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100236 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000237 help="directory where reports are stored, default is reports",
238 )
239 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100240 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000241 help="keep all reports, even if there are no compatibility issues",
242 )
243 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100244 "-o", "--old-rev", type=str, help="revision for old version",
Darryl Green7c2dd582018-03-01 14:53:49 +0000245 required=True
246 )
247 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100248 "-n", "--new-rev", type=str, help="revision for new version",
Darryl Green7c2dd582018-03-01 14:53:49 +0000249 required=True
250 )
251 abi_args = parser.parse_args()
252 abi_check = AbiChecker(
253 abi_args.report_dir, abi_args.old_rev,
254 abi_args.new_rev, abi_args.keep_all_reports
255 )
256 return_code = abi_check.check_for_abi_changes()
257 sys.exit(return_code)
Gilles Peskineafd19dd2019-02-25 21:39:42 +0100258 except Exception: # pylint: disable=broad-except
259 # Print the backtrace and exit explicitly so as to exit with
260 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000261 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000262 sys.exit(2)
263
264
265if __name__ == "__main__":
266 run_main()