blob: fcd40b713ee3864cc436a6b0c25e5aa418f7e11c [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
51 self.should_keep_report_dir = os.path.isdir(self.report_dir)
Darryl Green834ebc42019-02-19 16:59:33 +000052 self.old_repo = old_repo
Darryl Green3da15042018-03-01 14:53:49 +000053 self.old_rev = old_rev
Darryl Green834ebc42019-02-19 16:59:33 +000054 self.new_repo = new_repo
Darryl Green3da15042018-03-01 14:53:49 +000055 self.new_rev = new_rev
Darryl Greend3cde6f2019-02-20 15:01:56 +000056 self.skip_file = skip_file
Darryl Green32e7a502019-02-21 13:09:26 +000057 self.brief = brief
Darryl Green3da15042018-03-01 14:53:49 +000058 self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
59 self.old_dumps = {}
60 self.new_dumps = {}
61 self.git_command = "git"
62 self.make_command = "make"
63
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010064 @staticmethod
65 def check_repo_path():
Darryl Greenc47ac262018-03-15 10:12:06 +000066 current_dir = os.path.realpath('.')
67 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
68 if current_dir != root_dir:
Darryl Green3da15042018-03-01 14:53:49 +000069 raise Exception("Must be run from Mbed TLS root")
70
71 def setup_logger(self):
72 self.log = logging.getLogger()
73 self.log.setLevel(logging.INFO)
74 self.log.addHandler(logging.StreamHandler())
75
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010076 @staticmethod
77 def check_abi_tools_are_installed():
Darryl Green3da15042018-03-01 14:53:49 +000078 for command in ["abi-dumper", "abi-compliance-checker"]:
79 if not shutil.which(command):
80 raise Exception("{} not installed, aborting".format(command))
81
Darryl Green834ebc42019-02-19 16:59:33 +000082 def get_clean_worktree_for_git_revision(self, remote_repo, git_rev):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +010083 """Make a separate worktree with git_rev checked out.
84 Do not modify the current worktree."""
Darryl Green3da15042018-03-01 14:53:49 +000085 git_worktree_path = tempfile.mkdtemp()
Darryl Green834ebc42019-02-19 16:59:33 +000086 if remote_repo:
87 self.log.info(
88 "Checking out git worktree for revision {} from {}".format(
89 git_rev, remote_repo
90 )
91 )
92 fetch_process = subprocess.Popen(
93 [self.git_command, "fetch", remote_repo, git_rev],
94 cwd=self.repo_path,
95 stdout=subprocess.PIPE,
96 stderr=subprocess.STDOUT
97 )
98 fetch_output, _ = fetch_process.communicate()
99 self.log.info(fetch_output.decode("utf-8"))
100 if fetch_process.returncode != 0:
101 raise Exception("Fetching revision failed, aborting")
102 worktree_rev = "FETCH_HEAD"
103 else:
104 self.log.info(
105 "Checking out git worktree for revision {}".format(git_rev)
106 )
107 worktree_rev = git_rev
Darryl Green3da15042018-03-01 14:53:49 +0000108 worktree_process = subprocess.Popen(
Darryl Green834ebc42019-02-19 16:59:33 +0000109 [self.git_command, "worktree", "add", "--detach",
110 git_worktree_path, worktree_rev],
Darryl Green3da15042018-03-01 14:53:49 +0000111 cwd=self.repo_path,
112 stdout=subprocess.PIPE,
113 stderr=subprocess.STDOUT
114 )
115 worktree_output, _ = worktree_process.communicate()
116 self.log.info(worktree_output.decode("utf-8"))
117 if worktree_process.returncode != 0:
118 raise Exception("Checking out worktree failed, aborting")
119 return git_worktree_path
120
Jaeden Amero346f9592018-11-02 16:35:09 +0000121 def update_git_submodules(self, git_worktree_path):
122 process = subprocess.Popen(
123 [self.git_command, "submodule", "update", "--init", '--recursive'],
124 cwd=git_worktree_path,
125 stdout=subprocess.PIPE,
126 stderr=subprocess.STDOUT
127 )
128 output, _ = process.communicate()
129 self.log.info(output.decode("utf-8"))
130 if process.returncode != 0:
131 raise Exception("git submodule update failed, aborting")
132
Darryl Green3da15042018-03-01 14:53:49 +0000133 def build_shared_libraries(self, git_worktree_path):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100134 """Build the shared libraries in the specified worktree."""
Darryl Green3da15042018-03-01 14:53:49 +0000135 my_environment = os.environ.copy()
136 my_environment["CFLAGS"] = "-g -Og"
137 my_environment["SHARED"] = "1"
138 make_process = subprocess.Popen(
139 self.make_command,
140 env=my_environment,
141 cwd=git_worktree_path,
142 stdout=subprocess.PIPE,
143 stderr=subprocess.STDOUT
144 )
145 make_output, _ = make_process.communicate()
146 self.log.info(make_output.decode("utf-8"))
147 if make_process.returncode != 0:
148 raise Exception("make failed, aborting")
149
150 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100151 """Generate the ABI dumps for the specified git revision.
152 It must be checked out in git_worktree_path and the shared libraries
153 must have been built."""
Darryl Green3da15042018-03-01 14:53:49 +0000154 abi_dumps = {}
155 for mbed_module in self.mbedtls_modules:
156 output_path = os.path.join(
157 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
158 )
159 abi_dump_command = [
160 "abi-dumper",
161 os.path.join(
162 git_worktree_path, "library", mbed_module + ".so"),
163 "-o", output_path,
164 "-lver", git_ref
165 ]
166 abi_dump_process = subprocess.Popen(
167 abi_dump_command,
168 stdout=subprocess.PIPE,
169 stderr=subprocess.STDOUT
170 )
171 abi_dump_output, _ = abi_dump_process.communicate()
172 self.log.info(abi_dump_output.decode("utf-8"))
173 if abi_dump_process.returncode != 0:
174 raise Exception("abi-dumper failed, aborting")
175 abi_dumps[mbed_module] = output_path
176 return abi_dumps
177
178 def cleanup_worktree(self, git_worktree_path):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100179 """Remove the specified git worktree."""
Darryl Green3da15042018-03-01 14:53:49 +0000180 shutil.rmtree(git_worktree_path)
181 worktree_process = subprocess.Popen(
182 [self.git_command, "worktree", "prune"],
183 cwd=self.repo_path,
184 stdout=subprocess.PIPE,
185 stderr=subprocess.STDOUT
186 )
187 worktree_output, _ = worktree_process.communicate()
188 self.log.info(worktree_output.decode("utf-8"))
189 if worktree_process.returncode != 0:
190 raise Exception("Worktree cleanup failed, aborting")
191
Darryl Green834ebc42019-02-19 16:59:33 +0000192 def get_abi_dump_for_ref(self, remote_repo, git_rev):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100193 """Generate the ABI dumps for the specified git revision."""
Darryl Green834ebc42019-02-19 16:59:33 +0000194 git_worktree_path = self.get_clean_worktree_for_git_revision(
195 remote_repo, git_rev
196 )
Jaeden Amero346f9592018-11-02 16:35:09 +0000197 self.update_git_submodules(git_worktree_path)
Darryl Green3da15042018-03-01 14:53:49 +0000198 self.build_shared_libraries(git_worktree_path)
199 abi_dumps = self.get_abi_dumps_from_shared_libraries(
200 git_rev, git_worktree_path
201 )
202 self.cleanup_worktree(git_worktree_path)
203 return abi_dumps
204
Darryl Green32e7a502019-02-21 13:09:26 +0000205 def remove_children_with_tag(self, parent, tag):
206 children = parent.getchildren()
207 for child in children:
208 if child.tag == tag:
209 parent.remove(child)
210 else:
211 self.remove_children_with_tag(child, tag)
212
213 def remove_extra_detail_from_report(self, report_root):
214 for tag in ['test_info', 'test_results', 'problem_summary',
215 'added_symbols', 'removed_symbols', 'affected']:
216 self.remove_children_with_tag(report_root, tag)
217
218 for report in report_root:
219 for problems in report.getchildren()[:]:
220 if not problems.getchildren():
221 report.remove(problems)
222
Darryl Green3da15042018-03-01 14:53:49 +0000223 def get_abi_compatibility_report(self):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100224 """Generate a report of the differences between the reference ABI
225 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
226 be available."""
Darryl Green3da15042018-03-01 14:53:49 +0000227 compatibility_report = ""
228 compliance_return_code = 0
229 for mbed_module in self.mbedtls_modules:
230 output_path = os.path.join(
231 self.report_dir, "{}-{}-{}.html".format(
232 mbed_module, self.old_rev, self.new_rev
233 )
234 )
235 abi_compliance_command = [
236 "abi-compliance-checker",
237 "-l", mbed_module,
238 "-old", self.old_dumps[mbed_module],
239 "-new", self.new_dumps[mbed_module],
240 "-strict",
Darryl Green32e7a502019-02-21 13:09:26 +0000241 "-report-path", output_path,
Darryl Green3da15042018-03-01 14:53:49 +0000242 ]
Darryl Greend3cde6f2019-02-20 15:01:56 +0000243 if self.skip_file:
244 abi_compliance_command += ["-skip-symbols", self.skip_file,
245 "-skip-types", self.skip_file]
Darryl Green32e7a502019-02-21 13:09:26 +0000246 if self.brief:
247 abi_compliance_command += ["-report-format", "xml",
248 "-stdout"]
Darryl Green3da15042018-03-01 14:53:49 +0000249 abi_compliance_process = subprocess.Popen(
250 abi_compliance_command,
251 stdout=subprocess.PIPE,
252 stderr=subprocess.STDOUT
253 )
254 abi_compliance_output, _ = abi_compliance_process.communicate()
Darryl Green3da15042018-03-01 14:53:49 +0000255 if abi_compliance_process.returncode == 0:
256 compatibility_report += (
257 "No compatibility issues for {}\n".format(mbed_module)
258 )
Darryl Green32e7a502019-02-21 13:09:26 +0000259 if not (self.keep_all_reports or self.brief):
Darryl Green3da15042018-03-01 14:53:49 +0000260 os.remove(output_path)
261 elif abi_compliance_process.returncode == 1:
Darryl Green32e7a502019-02-21 13:09:26 +0000262 if self.brief:
263 self.log.info(
264 "Compatibility issues found for {}".format(mbed_module)
265 )
266 report_root = ET.fromstring(abi_compliance_output.decode("utf-8"))
267 self.remove_extra_detail_from_report(report_root)
268 self.log.info(ET.tostring(report_root).decode("utf-8"))
269 else:
270 compliance_return_code = 1
271 self.can_remove_report_dir = False
272 compatibility_report += (
273 "Compatibility issues found for {}, "
274 "for details see {}\n".format(mbed_module, output_path)
275 )
Darryl Green3da15042018-03-01 14:53:49 +0000276 else:
277 raise Exception(
278 "abi-compliance-checker failed with a return code of {},"
279 " aborting".format(abi_compliance_process.returncode)
280 )
281 os.remove(self.old_dumps[mbed_module])
282 os.remove(self.new_dumps[mbed_module])
283 if not self.should_keep_report_dir and not self.keep_all_reports:
284 os.rmdir(self.report_dir)
285 self.log.info(compatibility_report)
286 return compliance_return_code
287
288 def check_for_abi_changes(self):
Gilles Peskinefceb4ce2019-02-25 20:36:52 +0100289 """Generate a report of ABI differences
290 between self.old_rev and self.new_rev."""
Darryl Green3da15042018-03-01 14:53:49 +0000291 self.check_repo_path()
292 self.check_abi_tools_are_installed()
Darryl Green834ebc42019-02-19 16:59:33 +0000293 self.old_dumps = self.get_abi_dump_for_ref(self.old_repo, self.old_rev)
294 self.new_dumps = self.get_abi_dump_for_ref(self.new_repo, self.new_rev)
Darryl Green3da15042018-03-01 14:53:49 +0000295 return self.get_abi_compatibility_report()
296
297
298def run_main():
299 try:
300 parser = argparse.ArgumentParser(
301 description=(
Darryl Green31321ca2018-04-16 12:02:29 +0100302 """This script is a small wrapper around the
303 abi-compliance-checker and abi-dumper tools, applying them
304 to compare the ABI and API of the library files from two
305 different Git revisions within an Mbed TLS repository.
Darryl Green32e7a502019-02-21 13:09:26 +0000306 The results of the comparison are either formatted as HTML and
307 stored at a configurable location, or a brief list of problems
308 found is output. Returns 0 on success, 1 on ABI/API
Darryl Green31321ca2018-04-16 12:02:29 +0100309 non-compliance, and 2 if there is an error while running the
310 script. Note: must be run from Mbed TLS root."""
Darryl Green3da15042018-03-01 14:53:49 +0000311 )
312 )
313 parser.add_argument(
Darryl Green31321ca2018-04-16 12:02:29 +0100314 "-r", "--report-dir", type=str, default="reports",
Darryl Green3da15042018-03-01 14:53:49 +0000315 help="directory where reports are stored, default is reports",
316 )
317 parser.add_argument(
Darryl Green31321ca2018-04-16 12:02:29 +0100318 "-k", "--keep-all-reports", action="store_true",
Darryl Green3da15042018-03-01 14:53:49 +0000319 help="keep all reports, even if there are no compatibility issues",
320 )
321 parser.add_argument(
Darryl Green834ebc42019-02-19 16:59:33 +0000322 "-o", "--old-rev", type=str,
323 help=("revision for old version."
324 "Can include repository before revision"),
325 required=True, nargs="+"
Darryl Green3da15042018-03-01 14:53:49 +0000326 )
327 parser.add_argument(
Darryl Green834ebc42019-02-19 16:59:33 +0000328 "-n", "--new-rev", type=str,
329 help=("revision for new version"
330 "Can include repository before revision"),
331 required=True, nargs="+"
Darryl Green3da15042018-03-01 14:53:49 +0000332 )
Darryl Greend3cde6f2019-02-20 15:01:56 +0000333 parser.add_argument(
334 "-s", "--skip-file", type=str,
335 help="path to file containing symbols and types to skip"
336 )
Darryl Green32e7a502019-02-21 13:09:26 +0000337 parser.add_argument(
338 "-b", "--brief", action="store_true",
339 help="output only the list of issues to stdout, instead of a full report",
340 )
Darryl Green3da15042018-03-01 14:53:49 +0000341 abi_args = parser.parse_args()
Darryl Green834ebc42019-02-19 16:59:33 +0000342 if len(abi_args.old_rev) == 1:
343 old_repo = None
344 old_rev = abi_args.old_rev[0]
345 elif len(abi_args.old_rev) == 2:
346 old_repo = abi_args.old_rev[0]
347 old_rev = abi_args.old_rev[1]
348 else:
349 raise Exception("Too many arguments passed for old version")
350 if len(abi_args.new_rev) == 1:
351 new_repo = None
352 new_rev = abi_args.new_rev[0]
353 elif len(abi_args.new_rev) == 2:
354 new_repo = abi_args.new_rev[0]
355 new_rev = abi_args.new_rev[1]
356 else:
357 raise Exception("Too many arguments passed for new version")
Darryl Green3da15042018-03-01 14:53:49 +0000358 abi_check = AbiChecker(
Darryl Green834ebc42019-02-19 16:59:33 +0000359 abi_args.report_dir, old_repo, old_rev,
Darryl Greend3cde6f2019-02-20 15:01:56 +0000360 new_repo, new_rev, abi_args.keep_all_reports,
Darryl Green32e7a502019-02-21 13:09:26 +0000361 abi_args.brief, abi_args.skip_file
Darryl Green3da15042018-03-01 14:53:49 +0000362 )
363 return_code = abi_check.check_for_abi_changes()
364 sys.exit(return_code)
Darryl Greenc47ac262018-03-15 10:12:06 +0000365 except Exception:
366 traceback.print_exc()
Darryl Green3da15042018-03-01 14:53:49 +0000367 sys.exit(2)
368
369
370if __name__ == "__main__":
371 run_main()