blob: fbe31dd6cff413ffc1e5483ba94134dd2b773a94 [file] [log] [blame]
Darryl Green3da15042018-03-01 14:53:49 +00001#!/usr/bin/env python3
Darryl Green4cd7a9b2018-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 Green32e7a502019-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 Green4cd7a9b2018-04-06 11:23:22 +010016"""
Darryl Green3da15042018-03-01 14:53:49 +000017
18import os
19import sys
20import traceback
21import shutil
22import subprocess
23import argparse
24import logging
25import tempfile
Darryl Greend9ad9ec2019-02-25 11:35:05 +000026import fnmatch
Darryl Green3da15042018-03-01 14:53:49 +000027
Darryl Green32e7a502019-02-21 13:09:26 +000028import xml.etree.ElementTree as ET
29
Darryl Green3da15042018-03-01 14:53:49 +000030
31class AbiChecker(object):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010032 """API and ABI checker."""
Darryl Green3da15042018-03-01 14:53:49 +000033
Darryl Greend9ad9ec2019-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 Peskinefceb4ce2019-02-25 20:36:52 +010037 """Instantiate the API/ABI checker.
38
39 report_dir: directory for output files
Darryl Green834ebc42019-02-19 16:59:33 +000040 old_repo: repository for git revision to compare against
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010041 old_rev: reference git revision to compare against
Darryl Greend9ad9ec2019-02-25 11:35:05 +000042 old_crypto_rev: reference git revision for old crypto submodule
Darryl Green834ebc42019-02-19 16:59:33 +000043 new_repo: repository for git revision to check
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010044 new_rev: git revision to check
Darryl Greend9ad9ec2019-02-25 11:35:05 +000045 new_crypto_rev: reference git revision for new crypto submodule
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010046 keep_all_reports: if false, delete old reports
Darryl Green32e7a502019-02-21 13:09:26 +000047 brief: if true, output shorter report to stdout
Darryl Greend3cde6f2019-02-20 15:01:56 +000048 skip_file: path to file containing symbols and types to skip
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010049 """
Darryl Green3da15042018-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 Greenab3893b2019-02-25 17:01:55 +000055 self.can_remove_report_dir = not (os.path.isdir(self.report_dir) or
56 keep_all_reports)
Darryl Green834ebc42019-02-19 16:59:33 +000057 self.old_repo = old_repo
Darryl Green3da15042018-03-01 14:53:49 +000058 self.old_rev = old_rev
Darryl Greend9ad9ec2019-02-25 11:35:05 +000059 self.old_crypto_rev = old_crypto_rev
Darryl Green834ebc42019-02-19 16:59:33 +000060 self.new_repo = new_repo
Darryl Green3da15042018-03-01 14:53:49 +000061 self.new_rev = new_rev
Darryl Greend9ad9ec2019-02-25 11:35:05 +000062 self.new_crypto_rev = new_crypto_rev
Darryl Greend3cde6f2019-02-20 15:01:56 +000063 self.skip_file = skip_file
Darryl Green32e7a502019-02-21 13:09:26 +000064 self.brief = brief
Darryl Greend98d8b52019-02-27 16:53:40 +000065 self.mbedtls_modules = {"old": {}, "new": {}}
Darryl Green3da15042018-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 Peskinefceb4ce2019-02-25 20:36:52 +010071 @staticmethod
72 def check_repo_path():
Darryl Greenc47ac262018-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 Green3da15042018-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 Peskinefceb4ce2019-02-25 20:36:52 +010083 @staticmethod
84 def check_abi_tools_are_installed():
Darryl Green3da15042018-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 Green834ebc42019-02-19 16:59:33 +000089 def get_clean_worktree_for_git_revision(self, remote_repo, git_rev):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010090 """Make a separate worktree with git_rev checked out.
91 Do not modify the current worktree."""
Darryl Green3da15042018-03-01 14:53:49 +000092 git_worktree_path = tempfile.mkdtemp()
Darryl Green834ebc42019-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 Green3da15042018-03-01 14:53:49 +0000115 worktree_process = subprocess.Popen(
Darryl Green834ebc42019-02-19 16:59:33 +0000116 [self.git_command, "worktree", "add", "--detach",
117 git_worktree_path, worktree_rev],
Darryl Green3da15042018-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 Greend9ad9ec2019-02-25 11:35:05 +0000128 def update_git_submodules(self, git_worktree_path, crypto_rev):
Jaeden Amero346f9592018-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 Greend9ad9ec2019-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 Amero346f9592018-11-02 16:35:09 +0000151
Darryl Greend98d8b52019-02-27 16:53:40 +0000152 def build_shared_libraries(self, git_worktree_path, version):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100153 """Build the shared libraries in the specified worktree."""
Darryl Green3da15042018-03-01 14:53:49 +0000154 my_environment = os.environ.copy()
155 my_environment["CFLAGS"] = "-g -Og"
156 my_environment["SHARED"] = "1"
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000157 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Green3da15042018-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 Greend9ad9ec2019-02-25 11:35:05 +0000167 for root, dirs, files in os.walk(git_worktree_path):
168 for file in fnmatch.filter(files, "*.so"):
Darryl Greend98d8b52019-02-27 16:53:40 +0000169 self.mbedtls_modules[version][os.path.splitext(file)[0]] = (
170 os.path.join(root, file)
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000171 )
Darryl Green3da15042018-03-01 14:53:49 +0000172 if make_process.returncode != 0:
173 raise Exception("make failed, aborting")
174
Darryl Greend98d8b52019-02-27 16:53:40 +0000175 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path,
176 version):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100177 """Generate the ABI dumps for the specified git revision.
178 It must be checked out in git_worktree_path and the shared libraries
179 must have been built."""
Darryl Green3da15042018-03-01 14:53:49 +0000180 abi_dumps = {}
Darryl Greend98d8b52019-02-27 16:53:40 +0000181 for mbed_module, module_path in self.mbedtls_modules[version].items():
Darryl Green3da15042018-03-01 14:53:49 +0000182 output_path = os.path.join(
Darryl Greend98d8b52019-02-27 16:53:40 +0000183 self.report_dir, version, "{}-{}.dump".format(
184 mbed_module, git_ref
185 )
Darryl Green3da15042018-03-01 14:53:49 +0000186 )
187 abi_dump_command = [
188 "abi-dumper",
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000189 module_path,
Darryl Green3da15042018-03-01 14:53:49 +0000190 "-o", output_path,
191 "-lver", git_ref
192 ]
193 abi_dump_process = subprocess.Popen(
194 abi_dump_command,
195 stdout=subprocess.PIPE,
196 stderr=subprocess.STDOUT
197 )
198 abi_dump_output, _ = abi_dump_process.communicate()
199 self.log.info(abi_dump_output.decode("utf-8"))
200 if abi_dump_process.returncode != 0:
201 raise Exception("abi-dumper failed, aborting")
202 abi_dumps[mbed_module] = output_path
203 return abi_dumps
204
205 def cleanup_worktree(self, git_worktree_path):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100206 """Remove the specified git worktree."""
Darryl Green3da15042018-03-01 14:53:49 +0000207 shutil.rmtree(git_worktree_path)
208 worktree_process = subprocess.Popen(
209 [self.git_command, "worktree", "prune"],
210 cwd=self.repo_path,
211 stdout=subprocess.PIPE,
212 stderr=subprocess.STDOUT
213 )
214 worktree_output, _ = worktree_process.communicate()
215 self.log.info(worktree_output.decode("utf-8"))
216 if worktree_process.returncode != 0:
217 raise Exception("Worktree cleanup failed, aborting")
218
Darryl Greend98d8b52019-02-27 16:53:40 +0000219 def get_abi_dump_for_ref(self, remote_repo, git_rev, crypto_rev, version):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100220 """Generate the ABI dumps for the specified git revision."""
Darryl Green834ebc42019-02-19 16:59:33 +0000221 git_worktree_path = self.get_clean_worktree_for_git_revision(
222 remote_repo, git_rev
223 )
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000224 self.update_git_submodules(git_worktree_path, crypto_rev)
Darryl Greend98d8b52019-02-27 16:53:40 +0000225 self.build_shared_libraries(git_worktree_path, version)
Darryl Green3da15042018-03-01 14:53:49 +0000226 abi_dumps = self.get_abi_dumps_from_shared_libraries(
Darryl Greend98d8b52019-02-27 16:53:40 +0000227 git_rev, git_worktree_path, version
Darryl Green3da15042018-03-01 14:53:49 +0000228 )
229 self.cleanup_worktree(git_worktree_path)
230 return abi_dumps
231
Darryl Green32e7a502019-02-21 13:09:26 +0000232 def remove_children_with_tag(self, parent, tag):
233 children = parent.getchildren()
234 for child in children:
235 if child.tag == tag:
236 parent.remove(child)
237 else:
238 self.remove_children_with_tag(child, tag)
239
240 def remove_extra_detail_from_report(self, report_root):
241 for tag in ['test_info', 'test_results', 'problem_summary',
242 'added_symbols', 'removed_symbols', 'affected']:
243 self.remove_children_with_tag(report_root, tag)
244
245 for report in report_root:
246 for problems in report.getchildren()[:]:
247 if not problems.getchildren():
248 report.remove(problems)
249
Darryl Green3da15042018-03-01 14:53:49 +0000250 def get_abi_compatibility_report(self):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100251 """Generate a report of the differences between the reference ABI
252 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
253 be available."""
Darryl Green3da15042018-03-01 14:53:49 +0000254 compatibility_report = ""
255 compliance_return_code = 0
Darryl Greend98d8b52019-02-27 16:53:40 +0000256 shared_modules = list(set(self.mbedtls_modules["old"].keys()) &
257 set(self.mbedtls_modules["new"].keys()))
258 for mbed_module in shared_modules:
Darryl Green3da15042018-03-01 14:53:49 +0000259 output_path = os.path.join(
260 self.report_dir, "{}-{}-{}.html".format(
261 mbed_module, self.old_rev, self.new_rev
262 )
263 )
264 abi_compliance_command = [
265 "abi-compliance-checker",
266 "-l", mbed_module,
267 "-old", self.old_dumps[mbed_module],
268 "-new", self.new_dumps[mbed_module],
269 "-strict",
Darryl Green32e7a502019-02-21 13:09:26 +0000270 "-report-path", output_path,
Darryl Green3da15042018-03-01 14:53:49 +0000271 ]
Darryl Greend3cde6f2019-02-20 15:01:56 +0000272 if self.skip_file:
273 abi_compliance_command += ["-skip-symbols", self.skip_file,
274 "-skip-types", self.skip_file]
Darryl Green32e7a502019-02-21 13:09:26 +0000275 if self.brief:
276 abi_compliance_command += ["-report-format", "xml",
277 "-stdout"]
Darryl Green3da15042018-03-01 14:53:49 +0000278 abi_compliance_process = subprocess.Popen(
279 abi_compliance_command,
280 stdout=subprocess.PIPE,
281 stderr=subprocess.STDOUT
282 )
283 abi_compliance_output, _ = abi_compliance_process.communicate()
Darryl Green3da15042018-03-01 14:53:49 +0000284 if abi_compliance_process.returncode == 0:
285 compatibility_report += (
286 "No compatibility issues for {}\n".format(mbed_module)
287 )
Darryl Green32e7a502019-02-21 13:09:26 +0000288 if not (self.keep_all_reports or self.brief):
Darryl Green3da15042018-03-01 14:53:49 +0000289 os.remove(output_path)
290 elif abi_compliance_process.returncode == 1:
Darryl Green32e7a502019-02-21 13:09:26 +0000291 if self.brief:
292 self.log.info(
293 "Compatibility issues found for {}".format(mbed_module)
294 )
295 report_root = ET.fromstring(abi_compliance_output.decode("utf-8"))
296 self.remove_extra_detail_from_report(report_root)
297 self.log.info(ET.tostring(report_root).decode("utf-8"))
298 else:
299 compliance_return_code = 1
300 self.can_remove_report_dir = False
301 compatibility_report += (
302 "Compatibility issues found for {}, "
303 "for details see {}\n".format(mbed_module, output_path)
304 )
Darryl Green3da15042018-03-01 14:53:49 +0000305 else:
306 raise Exception(
307 "abi-compliance-checker failed with a return code of {},"
308 " aborting".format(abi_compliance_process.returncode)
309 )
310 os.remove(self.old_dumps[mbed_module])
311 os.remove(self.new_dumps[mbed_module])
Darryl Greenab3893b2019-02-25 17:01:55 +0000312 if self.can_remove_report_dir:
Darryl Green3da15042018-03-01 14:53:49 +0000313 os.rmdir(self.report_dir)
314 self.log.info(compatibility_report)
315 return compliance_return_code
316
317 def check_for_abi_changes(self):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100318 """Generate a report of ABI differences
319 between self.old_rev and self.new_rev."""
Darryl Green3da15042018-03-01 14:53:49 +0000320 self.check_repo_path()
321 self.check_abi_tools_are_installed()
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000322 self.old_dumps = self.get_abi_dump_for_ref(self.old_repo, self.old_rev,
Darryl Greend98d8b52019-02-27 16:53:40 +0000323 self.old_crypto_rev, "old")
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000324 self.new_dumps = self.get_abi_dump_for_ref(self.new_repo, self.new_rev,
Darryl Greend98d8b52019-02-27 16:53:40 +0000325 self.new_crypto_rev, "new")
Darryl Green3da15042018-03-01 14:53:49 +0000326 return self.get_abi_compatibility_report()
327
328
329def run_main():
330 try:
331 parser = argparse.ArgumentParser(
332 description=(
Darryl Green31321ca2018-04-16 12:02:29 +0100333 """This script is a small wrapper around the
334 abi-compliance-checker and abi-dumper tools, applying them
335 to compare the ABI and API of the library files from two
336 different Git revisions within an Mbed TLS repository.
Darryl Green32e7a502019-02-21 13:09:26 +0000337 The results of the comparison are either formatted as HTML and
338 stored at a configurable location, or a brief list of problems
339 found is output. Returns 0 on success, 1 on ABI/API
Darryl Green31321ca2018-04-16 12:02:29 +0100340 non-compliance, and 2 if there is an error while running the
341 script. Note: must be run from Mbed TLS root."""
Darryl Green3da15042018-03-01 14:53:49 +0000342 )
343 )
344 parser.add_argument(
Darryl Green31321ca2018-04-16 12:02:29 +0100345 "-r", "--report-dir", type=str, default="reports",
Darryl Green3da15042018-03-01 14:53:49 +0000346 help="directory where reports are stored, default is reports",
347 )
348 parser.add_argument(
Darryl Green31321ca2018-04-16 12:02:29 +0100349 "-k", "--keep-all-reports", action="store_true",
Darryl Green3da15042018-03-01 14:53:49 +0000350 help="keep all reports, even if there are no compatibility issues",
351 )
352 parser.add_argument(
Darryl Green834ebc42019-02-19 16:59:33 +0000353 "-o", "--old-rev", type=str,
354 help=("revision for old version."
355 "Can include repository before revision"),
356 required=True, nargs="+"
Darryl Green3da15042018-03-01 14:53:49 +0000357 )
358 parser.add_argument(
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000359 "-oc", "--old-crypto-rev", type=str,
360 help="revision for old crypto version",
361 )
362 parser.add_argument(
Darryl Green834ebc42019-02-19 16:59:33 +0000363 "-n", "--new-rev", type=str,
364 help=("revision for new version"
365 "Can include repository before revision"),
366 required=True, nargs="+"
Darryl Green3da15042018-03-01 14:53:49 +0000367 )
Darryl Greend3cde6f2019-02-20 15:01:56 +0000368 parser.add_argument(
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000369 "-nc", "--new-crypto-rev", type=str,
370 help="revision for new crypto version",
371 )
372 parser.add_argument(
Darryl Greend3cde6f2019-02-20 15:01:56 +0000373 "-s", "--skip-file", type=str,
374 help="path to file containing symbols and types to skip"
375 )
Darryl Green32e7a502019-02-21 13:09:26 +0000376 parser.add_argument(
377 "-b", "--brief", action="store_true",
378 help="output only the list of issues to stdout, instead of a full report",
379 )
Darryl Green3da15042018-03-01 14:53:49 +0000380 abi_args = parser.parse_args()
Darryl Green834ebc42019-02-19 16:59:33 +0000381 if len(abi_args.old_rev) == 1:
382 old_repo = None
383 old_rev = abi_args.old_rev[0]
384 elif len(abi_args.old_rev) == 2:
385 old_repo = abi_args.old_rev[0]
386 old_rev = abi_args.old_rev[1]
387 else:
388 raise Exception("Too many arguments passed for old version")
389 if len(abi_args.new_rev) == 1:
390 new_repo = None
391 new_rev = abi_args.new_rev[0]
392 elif len(abi_args.new_rev) == 2:
393 new_repo = abi_args.new_rev[0]
394 new_rev = abi_args.new_rev[1]
395 else:
396 raise Exception("Too many arguments passed for new version")
Darryl Green3da15042018-03-01 14:53:49 +0000397 abi_check = AbiChecker(
Darryl Greend9ad9ec2019-02-25 11:35:05 +0000398 abi_args.report_dir, old_repo, old_rev, abi_args.old_crypto_rev,
399 new_repo, new_rev, abi_args.new_crypto_rev,
400 abi_args.keep_all_reports, abi_args.brief, abi_args.skip_file
Darryl Green3da15042018-03-01 14:53:49 +0000401 )
402 return_code = abi_check.check_for_abi_changes()
403 sys.exit(return_code)
Darryl Greenc47ac262018-03-15 10:12:06 +0000404 except Exception:
405 traceback.print_exc()
Darryl Green3da15042018-03-01 14:53:49 +0000406 sys.exit(2)
407
408
409if __name__ == "__main__":
410 run_main()