blob: 6475a7e645f6480499d9997a87a881ae000b381f [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.
Darryl Green0da45782019-02-21 13:09:26 +000012The results of the comparison are either formatted as HTML and stored at
13a configurable location, or a brief list of problems found is output.
14Returns 0 on success, 1 on ABI/API non-compliance, and 2 if there is an error
15while running the script. Note: 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
Darryl Greenae5d66c2019-02-25 11:35:05 +000026import fnmatch
Darryl Green7c2dd582018-03-01 14:53:49 +000027
Darryl Green0da45782019-02-21 13:09:26 +000028import xml.etree.ElementTree as ET
29
Darryl Green7c2dd582018-03-01 14:53:49 +000030
31class AbiChecker(object):
Gilles Peskine9df17632019-02-25 20:36:52 +010032 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000033
Darryl Greenae5d66c2019-02-25 11:35:05 +000034 def __init__(self, report_dir, old_repo, old_rev, old_crypto_rev,
35 new_repo, new_rev, new_crypto_rev, keep_all_reports, brief,
36 skip_file=None):
Gilles Peskine9df17632019-02-25 20:36:52 +010037 """Instantiate the API/ABI checker.
38
39 report_dir: directory for output files
Darryl Green5a301f02019-02-19 16:59:33 +000040 old_repo: repository for git revision to compare against
Gilles Peskine9df17632019-02-25 20:36:52 +010041 old_rev: reference git revision to compare against
Darryl Greenae5d66c2019-02-25 11:35:05 +000042 old_crypto_rev: reference git revision for old crypto submodule
Darryl Green5a301f02019-02-19 16:59:33 +000043 new_repo: repository for git revision to check
Gilles Peskine9df17632019-02-25 20:36:52 +010044 new_rev: git revision to check
Darryl Greenae5d66c2019-02-25 11:35:05 +000045 new_crypto_rev: reference git revision for new crypto submodule
Gilles Peskine9df17632019-02-25 20:36:52 +010046 keep_all_reports: if false, delete old reports
Darryl Green0da45782019-02-21 13:09:26 +000047 brief: if true, output shorter report to stdout
Darryl Green668063b2019-02-20 15:01:56 +000048 skip_file: path to file containing symbols and types to skip
Gilles Peskine9df17632019-02-25 20:36:52 +010049 """
Darryl Green7c2dd582018-03-01 14:53:49 +000050 self.repo_path = "."
51 self.log = None
52 self.setup_logger()
53 self.report_dir = os.path.abspath(report_dir)
54 self.keep_all_reports = keep_all_reports
Darryl Green131e24b2019-02-25 17:01:55 +000055 self.can_remove_report_dir = not (os.path.isdir(self.report_dir) or
56 keep_all_reports)
Darryl Green5a301f02019-02-19 16:59:33 +000057 self.old_repo = old_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000058 self.old_rev = old_rev
Darryl Greenae5d66c2019-02-25 11:35:05 +000059 self.old_crypto_rev = old_crypto_rev
Darryl Green5a301f02019-02-19 16:59:33 +000060 self.new_repo = new_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000061 self.new_rev = new_rev
Darryl Greenae5d66c2019-02-25 11:35:05 +000062 self.new_crypto_rev = new_crypto_rev
Darryl Green668063b2019-02-20 15:01:56 +000063 self.skip_file = skip_file
Darryl Green0da45782019-02-21 13:09:26 +000064 self.brief = brief
Darryl Greenae5d66c2019-02-25 11:35:05 +000065 self.mbedtls_modules = {}
Darryl Green7c2dd582018-03-01 14:53:49 +000066 self.old_dumps = {}
67 self.new_dumps = {}
68 self.git_command = "git"
69 self.make_command = "make"
70
Gilles Peskine9df17632019-02-25 20:36:52 +010071 @staticmethod
72 def check_repo_path():
Darryl Greena6f430f2018-03-15 10:12:06 +000073 current_dir = os.path.realpath('.')
74 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
75 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000076 raise Exception("Must be run from Mbed TLS root")
77
78 def setup_logger(self):
79 self.log = logging.getLogger()
80 self.log.setLevel(logging.INFO)
81 self.log.addHandler(logging.StreamHandler())
82
Gilles Peskine9df17632019-02-25 20:36:52 +010083 @staticmethod
84 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +000085 for command in ["abi-dumper", "abi-compliance-checker"]:
86 if not shutil.which(command):
87 raise Exception("{} not installed, aborting".format(command))
88
Darryl Green5a301f02019-02-19 16:59:33 +000089 def get_clean_worktree_for_git_revision(self, remote_repo, git_rev):
Gilles Peskine9df17632019-02-25 20:36:52 +010090 """Make a separate worktree with git_rev checked out.
91 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +000092 git_worktree_path = tempfile.mkdtemp()
Darryl Green5a301f02019-02-19 16:59:33 +000093 if remote_repo:
94 self.log.info(
95 "Checking out git worktree for revision {} from {}".format(
96 git_rev, remote_repo
97 )
98 )
99 fetch_process = subprocess.Popen(
100 [self.git_command, "fetch", remote_repo, git_rev],
101 cwd=self.repo_path,
102 stdout=subprocess.PIPE,
103 stderr=subprocess.STDOUT
104 )
105 fetch_output, _ = fetch_process.communicate()
106 self.log.info(fetch_output.decode("utf-8"))
107 if fetch_process.returncode != 0:
108 raise Exception("Fetching revision failed, aborting")
109 worktree_rev = "FETCH_HEAD"
110 else:
111 self.log.info(
112 "Checking out git worktree for revision {}".format(git_rev)
113 )
114 worktree_rev = git_rev
Darryl Green7c2dd582018-03-01 14:53:49 +0000115 worktree_process = subprocess.Popen(
Darryl Green5a301f02019-02-19 16:59:33 +0000116 [self.git_command, "worktree", "add", "--detach",
117 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000118 cwd=self.repo_path,
119 stdout=subprocess.PIPE,
120 stderr=subprocess.STDOUT
121 )
122 worktree_output, _ = worktree_process.communicate()
123 self.log.info(worktree_output.decode("utf-8"))
124 if worktree_process.returncode != 0:
125 raise Exception("Checking out worktree failed, aborting")
126 return git_worktree_path
127
Darryl Greenae5d66c2019-02-25 11:35:05 +0000128 def update_git_submodules(self, git_worktree_path, crypto_rev):
Jaeden Amero4cd4b4b2018-11-02 16:35:09 +0000129 process = subprocess.Popen(
130 [self.git_command, "submodule", "update", "--init", '--recursive'],
131 cwd=git_worktree_path,
132 stdout=subprocess.PIPE,
133 stderr=subprocess.STDOUT
134 )
135 output, _ = process.communicate()
136 self.log.info(output.decode("utf-8"))
137 if process.returncode != 0:
138 raise Exception("git submodule update failed, aborting")
Darryl Greenae5d66c2019-02-25 11:35:05 +0000139 if (os.path.exists(os.path.join(git_worktree_path, "crypto"))
140 and crypto_rev):
141 checkout_process = subprocess.Popen(
142 [self.git_command, "checkout", crypto_rev],
143 cwd=os.path.join(git_worktree_path, "crypto"),
144 stdout=subprocess.PIPE,
145 stderr=subprocess.STDOUT
146 )
147 checkout_output, _ = checkout_process.communicate()
148 self.log.info(checkout_output.decode("utf-8"))
149 if checkout_process.returncode != 0:
150 raise Exception("git checkout failed, aborting")
Jaeden Amero4cd4b4b2018-11-02 16:35:09 +0000151
Darryl Green7c2dd582018-03-01 14:53:49 +0000152 def build_shared_libraries(self, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +0100153 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000154 my_environment = os.environ.copy()
155 my_environment["CFLAGS"] = "-g -Og"
156 my_environment["SHARED"] = "1"
Darryl Greenae5d66c2019-02-25 11:35:05 +0000157 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Green7c2dd582018-03-01 14:53:49 +0000158 make_process = subprocess.Popen(
159 self.make_command,
160 env=my_environment,
161 cwd=git_worktree_path,
162 stdout=subprocess.PIPE,
163 stderr=subprocess.STDOUT
164 )
165 make_output, _ = make_process.communicate()
166 self.log.info(make_output.decode("utf-8"))
Darryl Greenae5d66c2019-02-25 11:35:05 +0000167 for root, dirs, files in os.walk(git_worktree_path):
168 for file in fnmatch.filter(files, "*.so"):
169 self.mbedtls_modules[os.path.splitext(file)[0]] = os.path.join(
170 root, file
171 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000172 if make_process.returncode != 0:
173 raise Exception("make failed, aborting")
174
175 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +0100176 """Generate the ABI dumps for the specified git revision.
177 It must be checked out in git_worktree_path and the shared libraries
178 must have been built."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000179 abi_dumps = {}
Darryl Greenae5d66c2019-02-25 11:35:05 +0000180 for mbed_module, module_path in self.mbedtls_modules.items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000181 output_path = os.path.join(
182 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
183 )
184 abi_dump_command = [
185 "abi-dumper",
Darryl Greenae5d66c2019-02-25 11:35:05 +0000186 module_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000187 "-o", output_path,
188 "-lver", git_ref
189 ]
190 abi_dump_process = subprocess.Popen(
191 abi_dump_command,
192 stdout=subprocess.PIPE,
193 stderr=subprocess.STDOUT
194 )
195 abi_dump_output, _ = abi_dump_process.communicate()
196 self.log.info(abi_dump_output.decode("utf-8"))
197 if abi_dump_process.returncode != 0:
198 raise Exception("abi-dumper failed, aborting")
199 abi_dumps[mbed_module] = output_path
200 return abi_dumps
201
202 def cleanup_worktree(self, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +0100203 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000204 shutil.rmtree(git_worktree_path)
205 worktree_process = subprocess.Popen(
206 [self.git_command, "worktree", "prune"],
207 cwd=self.repo_path,
208 stdout=subprocess.PIPE,
209 stderr=subprocess.STDOUT
210 )
211 worktree_output, _ = worktree_process.communicate()
212 self.log.info(worktree_output.decode("utf-8"))
213 if worktree_process.returncode != 0:
214 raise Exception("Worktree cleanup failed, aborting")
215
Darryl Greenae5d66c2019-02-25 11:35:05 +0000216 def get_abi_dump_for_ref(self, remote_repo, git_rev, crypto_rev):
Gilles Peskine9df17632019-02-25 20:36:52 +0100217 """Generate the ABI dumps for the specified git revision."""
Darryl Green5a301f02019-02-19 16:59:33 +0000218 git_worktree_path = self.get_clean_worktree_for_git_revision(
219 remote_repo, git_rev
220 )
Darryl Greenae5d66c2019-02-25 11:35:05 +0000221 self.update_git_submodules(git_worktree_path, crypto_rev)
Darryl Green7c2dd582018-03-01 14:53:49 +0000222 self.build_shared_libraries(git_worktree_path)
223 abi_dumps = self.get_abi_dumps_from_shared_libraries(
224 git_rev, git_worktree_path
225 )
226 self.cleanup_worktree(git_worktree_path)
227 return abi_dumps
228
Darryl Green0da45782019-02-21 13:09:26 +0000229 def remove_children_with_tag(self, parent, tag):
230 children = parent.getchildren()
231 for child in children:
232 if child.tag == tag:
233 parent.remove(child)
234 else:
235 self.remove_children_with_tag(child, tag)
236
237 def remove_extra_detail_from_report(self, report_root):
238 for tag in ['test_info', 'test_results', 'problem_summary',
239 'added_symbols', 'removed_symbols', 'affected']:
240 self.remove_children_with_tag(report_root, tag)
241
242 for report in report_root:
243 for problems in report.getchildren()[:]:
244 if not problems.getchildren():
245 report.remove(problems)
246
Darryl Green7c2dd582018-03-01 14:53:49 +0000247 def get_abi_compatibility_report(self):
Gilles Peskine9df17632019-02-25 20:36:52 +0100248 """Generate a report of the differences between the reference ABI
249 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
250 be available."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000251 compatibility_report = ""
252 compliance_return_code = 0
Darryl Greenae5d66c2019-02-25 11:35:05 +0000253 for mbed_module, module_path in self.mbedtls_modules.items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000254 output_path = os.path.join(
255 self.report_dir, "{}-{}-{}.html".format(
256 mbed_module, self.old_rev, self.new_rev
257 )
258 )
259 abi_compliance_command = [
260 "abi-compliance-checker",
261 "-l", mbed_module,
262 "-old", self.old_dumps[mbed_module],
263 "-new", self.new_dumps[mbed_module],
264 "-strict",
Darryl Green0da45782019-02-21 13:09:26 +0000265 "-report-path", output_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000266 ]
Darryl Green668063b2019-02-20 15:01:56 +0000267 if self.skip_file:
268 abi_compliance_command += ["-skip-symbols", self.skip_file,
269 "-skip-types", self.skip_file]
Darryl Green0da45782019-02-21 13:09:26 +0000270 if self.brief:
271 abi_compliance_command += ["-report-format", "xml",
272 "-stdout"]
Darryl Green7c2dd582018-03-01 14:53:49 +0000273 abi_compliance_process = subprocess.Popen(
274 abi_compliance_command,
275 stdout=subprocess.PIPE,
276 stderr=subprocess.STDOUT
277 )
278 abi_compliance_output, _ = abi_compliance_process.communicate()
Darryl Green7c2dd582018-03-01 14:53:49 +0000279 if abi_compliance_process.returncode == 0:
280 compatibility_report += (
281 "No compatibility issues for {}\n".format(mbed_module)
282 )
Darryl Green0da45782019-02-21 13:09:26 +0000283 if not (self.keep_all_reports or self.brief):
Darryl Green7c2dd582018-03-01 14:53:49 +0000284 os.remove(output_path)
285 elif abi_compliance_process.returncode == 1:
Darryl Green0da45782019-02-21 13:09:26 +0000286 if self.brief:
287 self.log.info(
288 "Compatibility issues found for {}".format(mbed_module)
289 )
290 report_root = ET.fromstring(abi_compliance_output.decode("utf-8"))
291 self.remove_extra_detail_from_report(report_root)
292 self.log.info(ET.tostring(report_root).decode("utf-8"))
293 else:
294 compliance_return_code = 1
295 self.can_remove_report_dir = False
296 compatibility_report += (
297 "Compatibility issues found for {}, "
298 "for details see {}\n".format(mbed_module, output_path)
299 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000300 else:
301 raise Exception(
302 "abi-compliance-checker failed with a return code of {},"
303 " aborting".format(abi_compliance_process.returncode)
304 )
305 os.remove(self.old_dumps[mbed_module])
306 os.remove(self.new_dumps[mbed_module])
Darryl Green131e24b2019-02-25 17:01:55 +0000307 if self.can_remove_report_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +0000308 os.rmdir(self.report_dir)
309 self.log.info(compatibility_report)
310 return compliance_return_code
311
312 def check_for_abi_changes(self):
Gilles Peskine9df17632019-02-25 20:36:52 +0100313 """Generate a report of ABI differences
314 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000315 self.check_repo_path()
316 self.check_abi_tools_are_installed()
Darryl Greenae5d66c2019-02-25 11:35:05 +0000317 self.old_dumps = self.get_abi_dump_for_ref(self.old_repo, self.old_rev,
318 self.old_crypto_rev)
319 self.new_dumps = self.get_abi_dump_for_ref(self.new_repo, self.new_rev,
320 self.new_crypto_rev)
Darryl Green7c2dd582018-03-01 14:53:49 +0000321 return self.get_abi_compatibility_report()
322
323
324def run_main():
325 try:
326 parser = argparse.ArgumentParser(
327 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100328 """This script is a small wrapper around the
329 abi-compliance-checker and abi-dumper tools, applying them
330 to compare the ABI and API of the library files from two
331 different Git revisions within an Mbed TLS repository.
Darryl Green0da45782019-02-21 13:09:26 +0000332 The results of the comparison are either formatted as HTML and
333 stored at a configurable location, or a brief list of problems
334 found is output. Returns 0 on success, 1 on ABI/API
Darryl Green418527b2018-04-16 12:02:29 +0100335 non-compliance, and 2 if there is an error while running the
336 script. Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000337 )
338 )
339 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100340 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000341 help="directory where reports are stored, default is reports",
342 )
343 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100344 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000345 help="keep all reports, even if there are no compatibility issues",
346 )
347 parser.add_argument(
Darryl Green5a301f02019-02-19 16:59:33 +0000348 "-o", "--old-rev", type=str,
349 help=("revision for old version."
350 "Can include repository before revision"),
351 required=True, nargs="+"
Darryl Green7c2dd582018-03-01 14:53:49 +0000352 )
353 parser.add_argument(
Darryl Greenae5d66c2019-02-25 11:35:05 +0000354 "-oc", "--old-crypto-rev", type=str,
355 help="revision for old crypto version",
356 )
357 parser.add_argument(
Darryl Green5a301f02019-02-19 16:59:33 +0000358 "-n", "--new-rev", type=str,
359 help=("revision for new version"
360 "Can include repository before revision"),
361 required=True, nargs="+"
Darryl Green7c2dd582018-03-01 14:53:49 +0000362 )
Darryl Green668063b2019-02-20 15:01:56 +0000363 parser.add_argument(
Darryl Greenae5d66c2019-02-25 11:35:05 +0000364 "-nc", "--new-crypto-rev", type=str,
365 help="revision for new crypto version",
366 )
367 parser.add_argument(
Darryl Green668063b2019-02-20 15:01:56 +0000368 "-s", "--skip-file", type=str,
369 help="path to file containing symbols and types to skip"
370 )
Darryl Green0da45782019-02-21 13:09:26 +0000371 parser.add_argument(
372 "-b", "--brief", action="store_true",
373 help="output only the list of issues to stdout, instead of a full report",
374 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000375 abi_args = parser.parse_args()
Darryl Green5a301f02019-02-19 16:59:33 +0000376 if len(abi_args.old_rev) == 1:
377 old_repo = None
378 old_rev = abi_args.old_rev[0]
379 elif len(abi_args.old_rev) == 2:
380 old_repo = abi_args.old_rev[0]
381 old_rev = abi_args.old_rev[1]
382 else:
383 raise Exception("Too many arguments passed for old version")
384 if len(abi_args.new_rev) == 1:
385 new_repo = None
386 new_rev = abi_args.new_rev[0]
387 elif len(abi_args.new_rev) == 2:
388 new_repo = abi_args.new_rev[0]
389 new_rev = abi_args.new_rev[1]
390 else:
391 raise Exception("Too many arguments passed for new version")
Darryl Green7c2dd582018-03-01 14:53:49 +0000392 abi_check = AbiChecker(
Darryl Greenae5d66c2019-02-25 11:35:05 +0000393 abi_args.report_dir, old_repo, old_rev, abi_args.old_crypto_rev,
394 new_repo, new_rev, abi_args.new_crypto_rev,
395 abi_args.keep_all_reports, abi_args.brief, abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000396 )
397 return_code = abi_check.check_for_abi_changes()
398 sys.exit(return_code)
Gilles Peskineafd19dd2019-02-25 21:39:42 +0100399 except Exception: # pylint: disable=broad-except
400 # Print the backtrace and exit explicitly so as to exit with
401 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000402 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000403 sys.exit(2)
404
405
406if __name__ == "__main__":
407 run_main()