blob: a94185229a0125a71ebb8d6e22b6b220230a7e33 [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
26
Darryl Green32e7a502019-02-21 13:09:26 +000027import xml.etree.ElementTree as ET
28
Darryl Green3da15042018-03-01 14:53:49 +000029
30class AbiChecker(object):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010031 """API and ABI checker."""
Darryl Green3da15042018-03-01 14:53:49 +000032
Darryl Green834ebc42019-02-19 16:59:33 +000033 def __init__(self, report_dir, old_repo, old_rev, new_repo, new_rev,
Darryl Green32e7a502019-02-21 13:09:26 +000034 keep_all_reports, brief, skip_file=None):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010035 """Instantiate the API/ABI checker.
36
37 report_dir: directory for output files
Darryl Green834ebc42019-02-19 16:59:33 +000038 old_repo: repository for git revision to compare against
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010039 old_rev: reference git revision to compare against
Darryl Green834ebc42019-02-19 16:59:33 +000040 new_repo: repository for git revision to check
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010041 new_rev: git revision to check
42 keep_all_reports: if false, delete old reports
Darryl Green32e7a502019-02-21 13:09:26 +000043 brief: if true, output shorter report to stdout
Darryl Greend3cde6f2019-02-20 15:01:56 +000044 skip_file: path to file containing symbols and types to skip
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010045 """
Darryl Green3da15042018-03-01 14:53:49 +000046 self.repo_path = "."
47 self.log = None
48 self.setup_logger()
49 self.report_dir = os.path.abspath(report_dir)
50 self.keep_all_reports = keep_all_reports
Darryl Greenab3893b2019-02-25 17:01:55 +000051 self.can_remove_report_dir = not (os.path.isdir(self.report_dir) or
52 keep_all_reports)
Darryl Green834ebc42019-02-19 16:59:33 +000053 self.old_repo = old_repo
Darryl Green3da15042018-03-01 14:53:49 +000054 self.old_rev = old_rev
Darryl Green834ebc42019-02-19 16:59:33 +000055 self.new_repo = new_repo
Darryl Green3da15042018-03-01 14:53:49 +000056 self.new_rev = new_rev
Darryl Greend3cde6f2019-02-20 15:01:56 +000057 self.skip_file = skip_file
Darryl Green32e7a502019-02-21 13:09:26 +000058 self.brief = brief
Darryl Green3da15042018-03-01 14:53:49 +000059 self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
60 self.old_dumps = {}
61 self.new_dumps = {}
62 self.git_command = "git"
63 self.make_command = "make"
64
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010065 @staticmethod
66 def check_repo_path():
Darryl Greenc47ac262018-03-15 10:12:06 +000067 current_dir = os.path.realpath('.')
68 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
69 if current_dir != root_dir:
Darryl Green3da15042018-03-01 14:53:49 +000070 raise Exception("Must be run from Mbed TLS root")
71
72 def setup_logger(self):
73 self.log = logging.getLogger()
74 self.log.setLevel(logging.INFO)
75 self.log.addHandler(logging.StreamHandler())
76
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010077 @staticmethod
78 def check_abi_tools_are_installed():
Darryl Green3da15042018-03-01 14:53:49 +000079 for command in ["abi-dumper", "abi-compliance-checker"]:
80 if not shutil.which(command):
81 raise Exception("{} not installed, aborting".format(command))
82
Darryl Green834ebc42019-02-19 16:59:33 +000083 def get_clean_worktree_for_git_revision(self, remote_repo, git_rev):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010084 """Make a separate worktree with git_rev checked out.
85 Do not modify the current worktree."""
Darryl Green3da15042018-03-01 14:53:49 +000086 git_worktree_path = tempfile.mkdtemp()
Darryl Green834ebc42019-02-19 16:59:33 +000087 if remote_repo:
88 self.log.info(
89 "Checking out git worktree for revision {} from {}".format(
90 git_rev, remote_repo
91 )
92 )
93 fetch_process = subprocess.Popen(
94 [self.git_command, "fetch", remote_repo, git_rev],
95 cwd=self.repo_path,
96 stdout=subprocess.PIPE,
97 stderr=subprocess.STDOUT
98 )
99 fetch_output, _ = fetch_process.communicate()
100 self.log.info(fetch_output.decode("utf-8"))
101 if fetch_process.returncode != 0:
102 raise Exception("Fetching revision failed, aborting")
103 worktree_rev = "FETCH_HEAD"
104 else:
105 self.log.info(
106 "Checking out git worktree for revision {}".format(git_rev)
107 )
108 worktree_rev = git_rev
Darryl Green3da15042018-03-01 14:53:49 +0000109 worktree_process = subprocess.Popen(
Darryl Green834ebc42019-02-19 16:59:33 +0000110 [self.git_command, "worktree", "add", "--detach",
111 git_worktree_path, worktree_rev],
Darryl Green3da15042018-03-01 14:53:49 +0000112 cwd=self.repo_path,
113 stdout=subprocess.PIPE,
114 stderr=subprocess.STDOUT
115 )
116 worktree_output, _ = worktree_process.communicate()
117 self.log.info(worktree_output.decode("utf-8"))
118 if worktree_process.returncode != 0:
119 raise Exception("Checking out worktree failed, aborting")
120 return git_worktree_path
121
Jaeden Amero346f9592018-11-02 16:35:09 +0000122 def update_git_submodules(self, git_worktree_path):
123 process = subprocess.Popen(
124 [self.git_command, "submodule", "update", "--init", '--recursive'],
125 cwd=git_worktree_path,
126 stdout=subprocess.PIPE,
127 stderr=subprocess.STDOUT
128 )
129 output, _ = process.communicate()
130 self.log.info(output.decode("utf-8"))
131 if process.returncode != 0:
132 raise Exception("git submodule update failed, aborting")
133
Darryl Green3da15042018-03-01 14:53:49 +0000134 def build_shared_libraries(self, git_worktree_path):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100135 """Build the shared libraries in the specified worktree."""
Darryl Green3da15042018-03-01 14:53:49 +0000136 my_environment = os.environ.copy()
137 my_environment["CFLAGS"] = "-g -Og"
138 my_environment["SHARED"] = "1"
139 make_process = subprocess.Popen(
140 self.make_command,
141 env=my_environment,
142 cwd=git_worktree_path,
143 stdout=subprocess.PIPE,
144 stderr=subprocess.STDOUT
145 )
146 make_output, _ = make_process.communicate()
147 self.log.info(make_output.decode("utf-8"))
148 if make_process.returncode != 0:
149 raise Exception("make failed, aborting")
150
151 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100152 """Generate the ABI dumps for the specified git revision.
153 It must be checked out in git_worktree_path and the shared libraries
154 must have been built."""
Darryl Green3da15042018-03-01 14:53:49 +0000155 abi_dumps = {}
156 for mbed_module in self.mbedtls_modules:
157 output_path = os.path.join(
158 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
159 )
160 abi_dump_command = [
161 "abi-dumper",
162 os.path.join(
163 git_worktree_path, "library", mbed_module + ".so"),
164 "-o", output_path,
165 "-lver", git_ref
166 ]
167 abi_dump_process = subprocess.Popen(
168 abi_dump_command,
169 stdout=subprocess.PIPE,
170 stderr=subprocess.STDOUT
171 )
172 abi_dump_output, _ = abi_dump_process.communicate()
173 self.log.info(abi_dump_output.decode("utf-8"))
174 if abi_dump_process.returncode != 0:
175 raise Exception("abi-dumper failed, aborting")
176 abi_dumps[mbed_module] = output_path
177 return abi_dumps
178
179 def cleanup_worktree(self, git_worktree_path):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100180 """Remove the specified git worktree."""
Darryl Green3da15042018-03-01 14:53:49 +0000181 shutil.rmtree(git_worktree_path)
182 worktree_process = subprocess.Popen(
183 [self.git_command, "worktree", "prune"],
184 cwd=self.repo_path,
185 stdout=subprocess.PIPE,
186 stderr=subprocess.STDOUT
187 )
188 worktree_output, _ = worktree_process.communicate()
189 self.log.info(worktree_output.decode("utf-8"))
190 if worktree_process.returncode != 0:
191 raise Exception("Worktree cleanup failed, aborting")
192
Darryl Green834ebc42019-02-19 16:59:33 +0000193 def get_abi_dump_for_ref(self, remote_repo, git_rev):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100194 """Generate the ABI dumps for the specified git revision."""
Darryl Green834ebc42019-02-19 16:59:33 +0000195 git_worktree_path = self.get_clean_worktree_for_git_revision(
196 remote_repo, git_rev
197 )
Jaeden Amero346f9592018-11-02 16:35:09 +0000198 self.update_git_submodules(git_worktree_path)
Darryl Green3da15042018-03-01 14:53:49 +0000199 self.build_shared_libraries(git_worktree_path)
200 abi_dumps = self.get_abi_dumps_from_shared_libraries(
201 git_rev, git_worktree_path
202 )
203 self.cleanup_worktree(git_worktree_path)
204 return abi_dumps
205
Darryl Green32e7a502019-02-21 13:09:26 +0000206 def remove_children_with_tag(self, parent, tag):
207 children = parent.getchildren()
208 for child in children:
209 if child.tag == tag:
210 parent.remove(child)
211 else:
212 self.remove_children_with_tag(child, tag)
213
214 def remove_extra_detail_from_report(self, report_root):
215 for tag in ['test_info', 'test_results', 'problem_summary',
216 'added_symbols', 'removed_symbols', 'affected']:
217 self.remove_children_with_tag(report_root, tag)
218
219 for report in report_root:
220 for problems in report.getchildren()[:]:
221 if not problems.getchildren():
222 report.remove(problems)
223
Darryl Green3da15042018-03-01 14:53:49 +0000224 def get_abi_compatibility_report(self):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100225 """Generate a report of the differences between the reference ABI
226 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
227 be available."""
Darryl Green3da15042018-03-01 14:53:49 +0000228 compatibility_report = ""
229 compliance_return_code = 0
230 for mbed_module in self.mbedtls_modules:
231 output_path = os.path.join(
232 self.report_dir, "{}-{}-{}.html".format(
233 mbed_module, self.old_rev, self.new_rev
234 )
235 )
236 abi_compliance_command = [
237 "abi-compliance-checker",
238 "-l", mbed_module,
239 "-old", self.old_dumps[mbed_module],
240 "-new", self.new_dumps[mbed_module],
241 "-strict",
Darryl Green32e7a502019-02-21 13:09:26 +0000242 "-report-path", output_path,
Darryl Green3da15042018-03-01 14:53:49 +0000243 ]
Darryl Greend3cde6f2019-02-20 15:01:56 +0000244 if self.skip_file:
245 abi_compliance_command += ["-skip-symbols", self.skip_file,
246 "-skip-types", self.skip_file]
Darryl Green32e7a502019-02-21 13:09:26 +0000247 if self.brief:
248 abi_compliance_command += ["-report-format", "xml",
249 "-stdout"]
Darryl Green3da15042018-03-01 14:53:49 +0000250 abi_compliance_process = subprocess.Popen(
251 abi_compliance_command,
252 stdout=subprocess.PIPE,
253 stderr=subprocess.STDOUT
254 )
255 abi_compliance_output, _ = abi_compliance_process.communicate()
Darryl Green3da15042018-03-01 14:53:49 +0000256 if abi_compliance_process.returncode == 0:
257 compatibility_report += (
258 "No compatibility issues for {}\n".format(mbed_module)
259 )
Darryl Green32e7a502019-02-21 13:09:26 +0000260 if not (self.keep_all_reports or self.brief):
Darryl Green3da15042018-03-01 14:53:49 +0000261 os.remove(output_path)
262 elif abi_compliance_process.returncode == 1:
Darryl Green32e7a502019-02-21 13:09:26 +0000263 if self.brief:
264 self.log.info(
265 "Compatibility issues found for {}".format(mbed_module)
266 )
267 report_root = ET.fromstring(abi_compliance_output.decode("utf-8"))
268 self.remove_extra_detail_from_report(report_root)
269 self.log.info(ET.tostring(report_root).decode("utf-8"))
270 else:
271 compliance_return_code = 1
272 self.can_remove_report_dir = False
273 compatibility_report += (
274 "Compatibility issues found for {}, "
275 "for details see {}\n".format(mbed_module, output_path)
276 )
Darryl Green3da15042018-03-01 14:53:49 +0000277 else:
278 raise Exception(
279 "abi-compliance-checker failed with a return code of {},"
280 " aborting".format(abi_compliance_process.returncode)
281 )
282 os.remove(self.old_dumps[mbed_module])
283 os.remove(self.new_dumps[mbed_module])
Darryl Greenab3893b2019-02-25 17:01:55 +0000284 if self.can_remove_report_dir:
Darryl Green3da15042018-03-01 14:53:49 +0000285 os.rmdir(self.report_dir)
286 self.log.info(compatibility_report)
287 return compliance_return_code
288
289 def check_for_abi_changes(self):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100290 """Generate a report of ABI differences
291 between self.old_rev and self.new_rev."""
Darryl Green3da15042018-03-01 14:53:49 +0000292 self.check_repo_path()
293 self.check_abi_tools_are_installed()
Darryl Green834ebc42019-02-19 16:59:33 +0000294 self.old_dumps = self.get_abi_dump_for_ref(self.old_repo, self.old_rev)
295 self.new_dumps = self.get_abi_dump_for_ref(self.new_repo, self.new_rev)
Darryl Green3da15042018-03-01 14:53:49 +0000296 return self.get_abi_compatibility_report()
297
298
299def run_main():
300 try:
301 parser = argparse.ArgumentParser(
302 description=(
Darryl Green31321ca2018-04-16 12:02:29 +0100303 """This script is a small wrapper around the
304 abi-compliance-checker and abi-dumper tools, applying them
305 to compare the ABI and API of the library files from two
306 different Git revisions within an Mbed TLS repository.
Darryl Green32e7a502019-02-21 13:09:26 +0000307 The results of the comparison are either formatted as HTML and
308 stored at a configurable location, or a brief list of problems
309 found is output. Returns 0 on success, 1 on ABI/API
Darryl Green31321ca2018-04-16 12:02:29 +0100310 non-compliance, and 2 if there is an error while running the
311 script. Note: must be run from Mbed TLS root."""
Darryl Green3da15042018-03-01 14:53:49 +0000312 )
313 )
314 parser.add_argument(
Darryl Green31321ca2018-04-16 12:02:29 +0100315 "-r", "--report-dir", type=str, default="reports",
Darryl Green3da15042018-03-01 14:53:49 +0000316 help="directory where reports are stored, default is reports",
317 )
318 parser.add_argument(
Darryl Green31321ca2018-04-16 12:02:29 +0100319 "-k", "--keep-all-reports", action="store_true",
Darryl Green3da15042018-03-01 14:53:49 +0000320 help="keep all reports, even if there are no compatibility issues",
321 )
322 parser.add_argument(
Darryl Green834ebc42019-02-19 16:59:33 +0000323 "-o", "--old-rev", type=str,
324 help=("revision for old version."
325 "Can include repository before revision"),
326 required=True, nargs="+"
Darryl Green3da15042018-03-01 14:53:49 +0000327 )
328 parser.add_argument(
Darryl Green834ebc42019-02-19 16:59:33 +0000329 "-n", "--new-rev", type=str,
330 help=("revision for new version"
331 "Can include repository before revision"),
332 required=True, nargs="+"
Darryl Green3da15042018-03-01 14:53:49 +0000333 )
Darryl Greend3cde6f2019-02-20 15:01:56 +0000334 parser.add_argument(
335 "-s", "--skip-file", type=str,
336 help="path to file containing symbols and types to skip"
337 )
Darryl Green32e7a502019-02-21 13:09:26 +0000338 parser.add_argument(
339 "-b", "--brief", action="store_true",
340 help="output only the list of issues to stdout, instead of a full report",
341 )
Darryl Green3da15042018-03-01 14:53:49 +0000342 abi_args = parser.parse_args()
Darryl Green834ebc42019-02-19 16:59:33 +0000343 if len(abi_args.old_rev) == 1:
344 old_repo = None
345 old_rev = abi_args.old_rev[0]
346 elif len(abi_args.old_rev) == 2:
347 old_repo = abi_args.old_rev[0]
348 old_rev = abi_args.old_rev[1]
349 else:
350 raise Exception("Too many arguments passed for old version")
351 if len(abi_args.new_rev) == 1:
352 new_repo = None
353 new_rev = abi_args.new_rev[0]
354 elif len(abi_args.new_rev) == 2:
355 new_repo = abi_args.new_rev[0]
356 new_rev = abi_args.new_rev[1]
357 else:
358 raise Exception("Too many arguments passed for new version")
Darryl Green3da15042018-03-01 14:53:49 +0000359 abi_check = AbiChecker(
Darryl Green834ebc42019-02-19 16:59:33 +0000360 abi_args.report_dir, old_repo, old_rev,
Darryl Greend3cde6f2019-02-20 15:01:56 +0000361 new_repo, new_rev, abi_args.keep_all_reports,
Darryl Green32e7a502019-02-21 13:09:26 +0000362 abi_args.brief, abi_args.skip_file
Darryl Green3da15042018-03-01 14:53:49 +0000363 )
364 return_code = abi_check.check_for_abi_changes()
365 sys.exit(return_code)
Darryl Greenc47ac262018-03-15 10:12:06 +0000366 except Exception:
367 traceback.print_exc()
Darryl Green3da15042018-03-01 14:53:49 +0000368 sys.exit(2)
369
370
371if __name__ == "__main__":
372 run_main()