blob: 559501deecc80f40032e598c8574e41c0ef2e451 [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 Peskine712afa72019-02-25 20:36:52 +010029 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000030
Darryl Greenda84e322019-02-19 16:59:33 +000031 def __init__(self, report_dir, old_repo, old_rev, new_repo, new_rev,
Darryl Greenc2883a22019-02-20 15:01:56 +000032 keep_all_reports, skip_file=None):
Gilles Peskine712afa72019-02-25 20:36:52 +010033 """Instantiate the API/ABI checker.
34
35 report_dir: directory for output files
Darryl Greenda84e322019-02-19 16:59:33 +000036 old_repo: repository for git revision to compare against
Gilles Peskine712afa72019-02-25 20:36:52 +010037 old_rev: reference git revision to compare against
Darryl Greenda84e322019-02-19 16:59:33 +000038 new_repo: repository for git revision to check
Gilles Peskine712afa72019-02-25 20:36:52 +010039 new_rev: git revision to check
40 keep_all_reports: if false, delete old reports
Darryl Greenc2883a22019-02-20 15:01:56 +000041 skip_file: path to file containing symbols and types to skip
Gilles Peskine712afa72019-02-25 20:36:52 +010042 """
Darryl Green7c2dd582018-03-01 14:53:49 +000043 self.repo_path = "."
44 self.log = None
45 self.setup_logger()
46 self.report_dir = os.path.abspath(report_dir)
47 self.keep_all_reports = keep_all_reports
48 self.should_keep_report_dir = os.path.isdir(self.report_dir)
Darryl Greenda84e322019-02-19 16:59:33 +000049 self.old_repo = old_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000050 self.old_rev = old_rev
Darryl Greenda84e322019-02-19 16:59:33 +000051 self.new_repo = new_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000052 self.new_rev = new_rev
Darryl Greenc2883a22019-02-20 15:01:56 +000053 self.skip_file = skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +000054 self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
55 self.old_dumps = {}
56 self.new_dumps = {}
57 self.git_command = "git"
58 self.make_command = "make"
59
Gilles Peskine712afa72019-02-25 20:36:52 +010060 @staticmethod
61 def check_repo_path():
Darryl Greena6f430f2018-03-15 10:12:06 +000062 current_dir = os.path.realpath('.')
63 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
64 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000065 raise Exception("Must be run from Mbed TLS root")
66
67 def setup_logger(self):
68 self.log = logging.getLogger()
69 self.log.setLevel(logging.INFO)
70 self.log.addHandler(logging.StreamHandler())
71
Gilles Peskine712afa72019-02-25 20:36:52 +010072 @staticmethod
73 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +000074 for command in ["abi-dumper", "abi-compliance-checker"]:
75 if not shutil.which(command):
76 raise Exception("{} not installed, aborting".format(command))
77
Darryl Greenda84e322019-02-19 16:59:33 +000078 def get_clean_worktree_for_git_revision(self, remote_repo, git_rev):
Gilles Peskine712afa72019-02-25 20:36:52 +010079 """Make a separate worktree with git_rev checked out.
80 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +000081 git_worktree_path = tempfile.mkdtemp()
Darryl Greenda84e322019-02-19 16:59:33 +000082 if remote_repo:
83 self.log.info(
84 "Checking out git worktree for revision {} from {}".format(
85 git_rev, remote_repo
86 )
87 )
88 fetch_process = subprocess.Popen(
89 [self.git_command, "fetch", remote_repo, git_rev],
90 cwd=self.repo_path,
91 stdout=subprocess.PIPE,
92 stderr=subprocess.STDOUT
93 )
94 fetch_output, _ = fetch_process.communicate()
95 self.log.info(fetch_output.decode("utf-8"))
96 if fetch_process.returncode != 0:
97 raise Exception("Fetching revision failed, aborting")
98 worktree_rev = "FETCH_HEAD"
99 else:
100 self.log.info(
101 "Checking out git worktree for revision {}".format(git_rev)
102 )
103 worktree_rev = git_rev
Darryl Green7c2dd582018-03-01 14:53:49 +0000104 worktree_process = subprocess.Popen(
Darryl Greenda84e322019-02-19 16:59:33 +0000105 [self.git_command, "worktree", "add", "--detach",
106 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000107 cwd=self.repo_path,
108 stdout=subprocess.PIPE,
109 stderr=subprocess.STDOUT
110 )
111 worktree_output, _ = worktree_process.communicate()
112 self.log.info(worktree_output.decode("utf-8"))
113 if worktree_process.returncode != 0:
114 raise Exception("Checking out worktree failed, aborting")
115 return git_worktree_path
116
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000117 def update_git_submodules(self, git_worktree_path):
118 process = subprocess.Popen(
119 [self.git_command, "submodule", "update", "--init", '--recursive'],
120 cwd=git_worktree_path,
121 stdout=subprocess.PIPE,
122 stderr=subprocess.STDOUT
123 )
124 output, _ = process.communicate()
125 self.log.info(output.decode("utf-8"))
126 if process.returncode != 0:
127 raise Exception("git submodule update failed, aborting")
128
Darryl Green7c2dd582018-03-01 14:53:49 +0000129 def build_shared_libraries(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100130 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000131 my_environment = os.environ.copy()
132 my_environment["CFLAGS"] = "-g -Og"
133 my_environment["SHARED"] = "1"
134 make_process = subprocess.Popen(
135 self.make_command,
136 env=my_environment,
137 cwd=git_worktree_path,
138 stdout=subprocess.PIPE,
139 stderr=subprocess.STDOUT
140 )
141 make_output, _ = make_process.communicate()
142 self.log.info(make_output.decode("utf-8"))
143 if make_process.returncode != 0:
144 raise Exception("make failed, aborting")
145
146 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100147 """Generate the ABI dumps for the specified git revision.
148 It must be checked out in git_worktree_path and the shared libraries
149 must have been built."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000150 abi_dumps = {}
151 for mbed_module in self.mbedtls_modules:
152 output_path = os.path.join(
153 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
154 )
155 abi_dump_command = [
156 "abi-dumper",
157 os.path.join(
158 git_worktree_path, "library", mbed_module + ".so"),
159 "-o", output_path,
160 "-lver", git_ref
161 ]
162 abi_dump_process = subprocess.Popen(
163 abi_dump_command,
164 stdout=subprocess.PIPE,
165 stderr=subprocess.STDOUT
166 )
167 abi_dump_output, _ = abi_dump_process.communicate()
168 self.log.info(abi_dump_output.decode("utf-8"))
169 if abi_dump_process.returncode != 0:
170 raise Exception("abi-dumper failed, aborting")
171 abi_dumps[mbed_module] = output_path
172 return abi_dumps
173
174 def cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100175 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000176 shutil.rmtree(git_worktree_path)
177 worktree_process = subprocess.Popen(
178 [self.git_command, "worktree", "prune"],
179 cwd=self.repo_path,
180 stdout=subprocess.PIPE,
181 stderr=subprocess.STDOUT
182 )
183 worktree_output, _ = worktree_process.communicate()
184 self.log.info(worktree_output.decode("utf-8"))
185 if worktree_process.returncode != 0:
186 raise Exception("Worktree cleanup failed, aborting")
187
Darryl Greenda84e322019-02-19 16:59:33 +0000188 def get_abi_dump_for_ref(self, remote_repo, git_rev):
Gilles Peskine712afa72019-02-25 20:36:52 +0100189 """Generate the ABI dumps for the specified git revision."""
Darryl Greenda84e322019-02-19 16:59:33 +0000190 git_worktree_path = self.get_clean_worktree_for_git_revision(
191 remote_repo, git_rev
192 )
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000193 self.update_git_submodules(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000194 self.build_shared_libraries(git_worktree_path)
195 abi_dumps = self.get_abi_dumps_from_shared_libraries(
196 git_rev, git_worktree_path
197 )
198 self.cleanup_worktree(git_worktree_path)
199 return abi_dumps
200
201 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100202 """Generate a report of the differences between the reference ABI
203 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
204 be available."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000205 compatibility_report = ""
206 compliance_return_code = 0
207 for mbed_module in self.mbedtls_modules:
208 output_path = os.path.join(
209 self.report_dir, "{}-{}-{}.html".format(
210 mbed_module, self.old_rev, self.new_rev
211 )
212 )
213 abi_compliance_command = [
214 "abi-compliance-checker",
215 "-l", mbed_module,
216 "-old", self.old_dumps[mbed_module],
217 "-new", self.new_dumps[mbed_module],
218 "-strict",
219 "-report-path", output_path
220 ]
Darryl Greenc2883a22019-02-20 15:01:56 +0000221 if self.skip_file:
222 abi_compliance_command += ["-skip-symbols", self.skip_file,
223 "-skip-types", self.skip_file]
Darryl Green7c2dd582018-03-01 14:53:49 +0000224 abi_compliance_process = subprocess.Popen(
225 abi_compliance_command,
226 stdout=subprocess.PIPE,
227 stderr=subprocess.STDOUT
228 )
229 abi_compliance_output, _ = abi_compliance_process.communicate()
230 self.log.info(abi_compliance_output.decode("utf-8"))
231 if abi_compliance_process.returncode == 0:
232 compatibility_report += (
233 "No compatibility issues for {}\n".format(mbed_module)
234 )
235 if not self.keep_all_reports:
236 os.remove(output_path)
237 elif abi_compliance_process.returncode == 1:
238 compliance_return_code = 1
239 self.should_keep_report_dir = True
240 compatibility_report += (
241 "Compatibility issues found for {}, "
242 "for details see {}\n".format(mbed_module, output_path)
243 )
244 else:
245 raise Exception(
246 "abi-compliance-checker failed with a return code of {},"
247 " aborting".format(abi_compliance_process.returncode)
248 )
249 os.remove(self.old_dumps[mbed_module])
250 os.remove(self.new_dumps[mbed_module])
251 if not self.should_keep_report_dir and not self.keep_all_reports:
252 os.rmdir(self.report_dir)
253 self.log.info(compatibility_report)
254 return compliance_return_code
255
256 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100257 """Generate a report of ABI differences
258 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000259 self.check_repo_path()
260 self.check_abi_tools_are_installed()
Darryl Greenda84e322019-02-19 16:59:33 +0000261 self.old_dumps = self.get_abi_dump_for_ref(self.old_repo, self.old_rev)
262 self.new_dumps = self.get_abi_dump_for_ref(self.new_repo, self.new_rev)
Darryl Green7c2dd582018-03-01 14:53:49 +0000263 return self.get_abi_compatibility_report()
264
265
266def run_main():
267 try:
268 parser = argparse.ArgumentParser(
269 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100270 """This script is a small wrapper around the
271 abi-compliance-checker and abi-dumper tools, applying them
272 to compare the ABI and API of the library files from two
273 different Git revisions within an Mbed TLS repository.
274 The results of the comparison are formatted as HTML and stored
275 at a configurable location. Returns 0 on success, 1 on ABI/API
276 non-compliance, and 2 if there is an error while running the
277 script. Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000278 )
279 )
280 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100281 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000282 help="directory where reports are stored, default is reports",
283 )
284 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100285 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000286 help="keep all reports, even if there are no compatibility issues",
287 )
288 parser.add_argument(
Darryl Greenda84e322019-02-19 16:59:33 +0000289 "-o", "--old-rev", type=str,
290 help=("revision for old version."
291 "Can include repository before revision"),
292 required=True, nargs="+"
Darryl Green7c2dd582018-03-01 14:53:49 +0000293 )
294 parser.add_argument(
Darryl Greenda84e322019-02-19 16:59:33 +0000295 "-n", "--new-rev", type=str,
296 help=("revision for new version"
297 "Can include repository before revision"),
298 required=True, nargs="+"
Darryl Green7c2dd582018-03-01 14:53:49 +0000299 )
Darryl Greenc2883a22019-02-20 15:01:56 +0000300 parser.add_argument(
301 "-s", "--skip-file", type=str,
302 help="path to file containing symbols and types to skip"
303 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000304 abi_args = parser.parse_args()
Darryl Greenda84e322019-02-19 16:59:33 +0000305 if len(abi_args.old_rev) == 1:
306 old_repo = None
307 old_rev = abi_args.old_rev[0]
308 elif len(abi_args.old_rev) == 2:
309 old_repo = abi_args.old_rev[0]
310 old_rev = abi_args.old_rev[1]
311 else:
312 raise Exception("Too many arguments passed for old version")
313 if len(abi_args.new_rev) == 1:
314 new_repo = None
315 new_rev = abi_args.new_rev[0]
316 elif len(abi_args.new_rev) == 2:
317 new_repo = abi_args.new_rev[0]
318 new_rev = abi_args.new_rev[1]
319 else:
320 raise Exception("Too many arguments passed for new version")
Darryl Green7c2dd582018-03-01 14:53:49 +0000321 abi_check = AbiChecker(
Darryl Greenda84e322019-02-19 16:59:33 +0000322 abi_args.report_dir, old_repo, old_rev,
Darryl Greenc2883a22019-02-20 15:01:56 +0000323 new_repo, new_rev, abi_args.keep_all_reports,
324 abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000325 )
326 return_code = abi_check.check_for_abi_changes()
327 sys.exit(return_code)
Gilles Peskinee915d532019-02-25 21:39:42 +0100328 except Exception: # pylint: disable=broad-except
329 # Print the backtrace and exit explicitly so as to exit with
330 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000331 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000332 sys.exit(2)
333
334
335if __name__ == "__main__":
336 run_main()