blob: d14236eadb6453f2416a6fa692f688c3dae79078 [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,
32 keep_all_reports):
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
41 """
Darryl Green7c2dd582018-03-01 14:53:49 +000042 self.repo_path = "."
43 self.log = None
44 self.setup_logger()
45 self.report_dir = os.path.abspath(report_dir)
46 self.keep_all_reports = keep_all_reports
47 self.should_keep_report_dir = os.path.isdir(self.report_dir)
Darryl Greenda84e322019-02-19 16:59:33 +000048 self.old_repo = old_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000049 self.old_rev = old_rev
Darryl Greenda84e322019-02-19 16:59:33 +000050 self.new_repo = new_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000051 self.new_rev = new_rev
52 self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
53 self.old_dumps = {}
54 self.new_dumps = {}
55 self.git_command = "git"
56 self.make_command = "make"
57
Gilles Peskine712afa72019-02-25 20:36:52 +010058 @staticmethod
59 def check_repo_path():
Darryl Greena6f430f2018-03-15 10:12:06 +000060 current_dir = os.path.realpath('.')
61 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
62 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000063 raise Exception("Must be run from Mbed TLS root")
64
65 def setup_logger(self):
66 self.log = logging.getLogger()
67 self.log.setLevel(logging.INFO)
68 self.log.addHandler(logging.StreamHandler())
69
Gilles Peskine712afa72019-02-25 20:36:52 +010070 @staticmethod
71 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +000072 for command in ["abi-dumper", "abi-compliance-checker"]:
73 if not shutil.which(command):
74 raise Exception("{} not installed, aborting".format(command))
75
Darryl Greenda84e322019-02-19 16:59:33 +000076 def get_clean_worktree_for_git_revision(self, remote_repo, git_rev):
Gilles Peskine712afa72019-02-25 20:36:52 +010077 """Make a separate worktree with git_rev checked out.
78 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +000079 git_worktree_path = tempfile.mkdtemp()
Darryl Greenda84e322019-02-19 16:59:33 +000080 if remote_repo:
81 self.log.info(
82 "Checking out git worktree for revision {} from {}".format(
83 git_rev, remote_repo
84 )
85 )
86 fetch_process = subprocess.Popen(
87 [self.git_command, "fetch", remote_repo, git_rev],
88 cwd=self.repo_path,
89 stdout=subprocess.PIPE,
90 stderr=subprocess.STDOUT
91 )
92 fetch_output, _ = fetch_process.communicate()
93 self.log.info(fetch_output.decode("utf-8"))
94 if fetch_process.returncode != 0:
95 raise Exception("Fetching revision failed, aborting")
96 worktree_rev = "FETCH_HEAD"
97 else:
98 self.log.info(
99 "Checking out git worktree for revision {}".format(git_rev)
100 )
101 worktree_rev = git_rev
Darryl Green7c2dd582018-03-01 14:53:49 +0000102 worktree_process = subprocess.Popen(
Darryl Greenda84e322019-02-19 16:59:33 +0000103 [self.git_command, "worktree", "add", "--detach",
104 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000105 cwd=self.repo_path,
106 stdout=subprocess.PIPE,
107 stderr=subprocess.STDOUT
108 )
109 worktree_output, _ = worktree_process.communicate()
110 self.log.info(worktree_output.decode("utf-8"))
111 if worktree_process.returncode != 0:
112 raise Exception("Checking out worktree failed, aborting")
113 return git_worktree_path
114
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000115 def update_git_submodules(self, git_worktree_path):
116 process = subprocess.Popen(
117 [self.git_command, "submodule", "update", "--init", '--recursive'],
118 cwd=git_worktree_path,
119 stdout=subprocess.PIPE,
120 stderr=subprocess.STDOUT
121 )
122 output, _ = process.communicate()
123 self.log.info(output.decode("utf-8"))
124 if process.returncode != 0:
125 raise Exception("git submodule update failed, aborting")
126
Darryl Green7c2dd582018-03-01 14:53:49 +0000127 def build_shared_libraries(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100128 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000129 my_environment = os.environ.copy()
130 my_environment["CFLAGS"] = "-g -Og"
131 my_environment["SHARED"] = "1"
132 make_process = subprocess.Popen(
133 self.make_command,
134 env=my_environment,
135 cwd=git_worktree_path,
136 stdout=subprocess.PIPE,
137 stderr=subprocess.STDOUT
138 )
139 make_output, _ = make_process.communicate()
140 self.log.info(make_output.decode("utf-8"))
141 if make_process.returncode != 0:
142 raise Exception("make failed, aborting")
143
144 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100145 """Generate the ABI dumps for the specified git revision.
146 It must be checked out in git_worktree_path and the shared libraries
147 must have been built."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000148 abi_dumps = {}
149 for mbed_module in self.mbedtls_modules:
150 output_path = os.path.join(
151 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
152 )
153 abi_dump_command = [
154 "abi-dumper",
155 os.path.join(
156 git_worktree_path, "library", mbed_module + ".so"),
157 "-o", output_path,
158 "-lver", git_ref
159 ]
160 abi_dump_process = subprocess.Popen(
161 abi_dump_command,
162 stdout=subprocess.PIPE,
163 stderr=subprocess.STDOUT
164 )
165 abi_dump_output, _ = abi_dump_process.communicate()
166 self.log.info(abi_dump_output.decode("utf-8"))
167 if abi_dump_process.returncode != 0:
168 raise Exception("abi-dumper failed, aborting")
169 abi_dumps[mbed_module] = output_path
170 return abi_dumps
171
172 def cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100173 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000174 shutil.rmtree(git_worktree_path)
175 worktree_process = subprocess.Popen(
176 [self.git_command, "worktree", "prune"],
177 cwd=self.repo_path,
178 stdout=subprocess.PIPE,
179 stderr=subprocess.STDOUT
180 )
181 worktree_output, _ = worktree_process.communicate()
182 self.log.info(worktree_output.decode("utf-8"))
183 if worktree_process.returncode != 0:
184 raise Exception("Worktree cleanup failed, aborting")
185
Darryl Greenda84e322019-02-19 16:59:33 +0000186 def get_abi_dump_for_ref(self, remote_repo, git_rev):
Gilles Peskine712afa72019-02-25 20:36:52 +0100187 """Generate the ABI dumps for the specified git revision."""
Darryl Greenda84e322019-02-19 16:59:33 +0000188 git_worktree_path = self.get_clean_worktree_for_git_revision(
189 remote_repo, git_rev
190 )
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000191 self.update_git_submodules(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000192 self.build_shared_libraries(git_worktree_path)
193 abi_dumps = self.get_abi_dumps_from_shared_libraries(
194 git_rev, git_worktree_path
195 )
196 self.cleanup_worktree(git_worktree_path)
197 return abi_dumps
198
199 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100200 """Generate a report of the differences between the reference ABI
201 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
202 be available."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000203 compatibility_report = ""
204 compliance_return_code = 0
205 for mbed_module in self.mbedtls_modules:
206 output_path = os.path.join(
207 self.report_dir, "{}-{}-{}.html".format(
208 mbed_module, self.old_rev, self.new_rev
209 )
210 )
211 abi_compliance_command = [
212 "abi-compliance-checker",
213 "-l", mbed_module,
214 "-old", self.old_dumps[mbed_module],
215 "-new", self.new_dumps[mbed_module],
216 "-strict",
217 "-report-path", output_path
218 ]
219 abi_compliance_process = subprocess.Popen(
220 abi_compliance_command,
221 stdout=subprocess.PIPE,
222 stderr=subprocess.STDOUT
223 )
224 abi_compliance_output, _ = abi_compliance_process.communicate()
225 self.log.info(abi_compliance_output.decode("utf-8"))
226 if abi_compliance_process.returncode == 0:
227 compatibility_report += (
228 "No compatibility issues for {}\n".format(mbed_module)
229 )
230 if not self.keep_all_reports:
231 os.remove(output_path)
232 elif abi_compliance_process.returncode == 1:
233 compliance_return_code = 1
234 self.should_keep_report_dir = True
235 compatibility_report += (
236 "Compatibility issues found for {}, "
237 "for details see {}\n".format(mbed_module, output_path)
238 )
239 else:
240 raise Exception(
241 "abi-compliance-checker failed with a return code of {},"
242 " aborting".format(abi_compliance_process.returncode)
243 )
244 os.remove(self.old_dumps[mbed_module])
245 os.remove(self.new_dumps[mbed_module])
246 if not self.should_keep_report_dir and not self.keep_all_reports:
247 os.rmdir(self.report_dir)
248 self.log.info(compatibility_report)
249 return compliance_return_code
250
251 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100252 """Generate a report of ABI differences
253 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000254 self.check_repo_path()
255 self.check_abi_tools_are_installed()
Darryl Greenda84e322019-02-19 16:59:33 +0000256 self.old_dumps = self.get_abi_dump_for_ref(self.old_repo, self.old_rev)
257 self.new_dumps = self.get_abi_dump_for_ref(self.new_repo, self.new_rev)
Darryl Green7c2dd582018-03-01 14:53:49 +0000258 return self.get_abi_compatibility_report()
259
260
261def run_main():
262 try:
263 parser = argparse.ArgumentParser(
264 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100265 """This script is a small wrapper around the
266 abi-compliance-checker and abi-dumper tools, applying them
267 to compare the ABI and API of the library files from two
268 different Git revisions within an Mbed TLS repository.
269 The results of the comparison are formatted as HTML and stored
270 at a configurable location. Returns 0 on success, 1 on ABI/API
271 non-compliance, and 2 if there is an error while running the
272 script. Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000273 )
274 )
275 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100276 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000277 help="directory where reports are stored, default is reports",
278 )
279 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100280 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000281 help="keep all reports, even if there are no compatibility issues",
282 )
283 parser.add_argument(
Darryl Greenda84e322019-02-19 16:59:33 +0000284 "-o", "--old-rev", type=str,
285 help=("revision for old version."
286 "Can include repository before revision"),
287 required=True, nargs="+"
Darryl Green7c2dd582018-03-01 14:53:49 +0000288 )
289 parser.add_argument(
Darryl Greenda84e322019-02-19 16:59:33 +0000290 "-n", "--new-rev", type=str,
291 help=("revision for new version"
292 "Can include repository before revision"),
293 required=True, nargs="+"
Darryl Green7c2dd582018-03-01 14:53:49 +0000294 )
295 abi_args = parser.parse_args()
Darryl Greenda84e322019-02-19 16:59:33 +0000296 if len(abi_args.old_rev) == 1:
297 old_repo = None
298 old_rev = abi_args.old_rev[0]
299 elif len(abi_args.old_rev) == 2:
300 old_repo = abi_args.old_rev[0]
301 old_rev = abi_args.old_rev[1]
302 else:
303 raise Exception("Too many arguments passed for old version")
304 if len(abi_args.new_rev) == 1:
305 new_repo = None
306 new_rev = abi_args.new_rev[0]
307 elif len(abi_args.new_rev) == 2:
308 new_repo = abi_args.new_rev[0]
309 new_rev = abi_args.new_rev[1]
310 else:
311 raise Exception("Too many arguments passed for new version")
Darryl Green7c2dd582018-03-01 14:53:49 +0000312 abi_check = AbiChecker(
Darryl Greenda84e322019-02-19 16:59:33 +0000313 abi_args.report_dir, old_repo, old_rev,
314 new_repo, new_rev, abi_args.keep_all_reports
Darryl Green7c2dd582018-03-01 14:53:49 +0000315 )
316 return_code = abi_check.check_for_abi_changes()
317 sys.exit(return_code)
Gilles Peskinee915d532019-02-25 21:39:42 +0100318 except Exception: # pylint: disable=broad-except
319 # Print the backtrace and exit explicitly so as to exit with
320 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000321 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000322 sys.exit(2)
323
324
325if __name__ == "__main__":
326 run_main()