3 This file is part of Mbed TLS (https://tls.mbed.org)
5 Copyright (c) 2018, Arm Limited, All Rights Reserved
9 This script is a small wrapper around the abi-compliance-checker and
10 abi-dumper tools, applying them to compare the ABI and API of the library
11 files from two different Git revisions within an Mbed TLS repository.
12 The results of the comparison are either formatted as HTML and stored at
13 a configurable location, or are given as a brief list of problems.
14 Returns 0 on success, 1 on ABI/API non-compliance, and 2 if there is an error
15 while running the script. Note: must be run from Mbed TLS root.
27 from types import SimpleNamespace
29 import xml.etree.ElementTree as ET
32 class AbiChecker(object):
33 """API and ABI checker."""
35 def __init__(self, old_version, new_version, configuration):
36 """Instantiate the API/ABI checker.
38 old_version: RepoVersion containing details to compare against
39 new_version: RepoVersion containing details to check
40 configuration.report_dir: directory for output files
41 configuration.keep_all_reports: if false, delete old reports
42 configuration.brief: if true, output shorter report to stdout
43 configuration.skip_file: path to file containing symbols and types to skip
47 self.verbose = configuration.verbose
49 self.report_dir = os.path.abspath(configuration.report_dir)
50 self.keep_all_reports = configuration.keep_all_reports
51 self.can_remove_report_dir = not (os.path.exists(self.report_dir) or
52 self.keep_all_reports)
53 self.old_version = old_version
54 self.new_version = new_version
55 self.skip_file = configuration.skip_file
56 self.brief = configuration.brief
57 self.git_command = "git"
58 self.make_command = "make"
61 def check_repo_path():
62 current_dir = os.path.realpath('.')
63 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
64 if current_dir != root_dir:
65 raise Exception("Must be run from Mbed TLS root")
67 def _setup_logger(self):
68 self.log = logging.getLogger()
70 self.log.setLevel(logging.DEBUG)
72 self.log.setLevel(logging.INFO)
73 self.log.addHandler(logging.StreamHandler())
76 def check_abi_tools_are_installed():
77 for command in ["abi-dumper", "abi-compliance-checker"]:
78 if not shutil.which(command):
79 raise Exception("{} not installed, aborting".format(command))
81 def _get_clean_worktree_for_git_revision(self, version):
82 """Make a separate worktree with version.revision checked out.
83 Do not modify the current worktree."""
84 git_worktree_path = tempfile.mkdtemp()
85 if version.repository:
87 "Checking out git worktree for revision {} from {}".format(
88 version.revision, version.repository
91 fetch_output = subprocess.check_output(
92 [self.git_command, "fetch",
93 version.repository, version.revision],
95 stderr=subprocess.STDOUT
97 self.log.debug(fetch_output.decode("utf-8"))
98 worktree_rev = "FETCH_HEAD"
100 self.log.debug("Checking out git worktree for revision {}".format(
103 worktree_rev = version.revision
104 worktree_output = subprocess.check_output(
105 [self.git_command, "worktree", "add", "--detach",
106 git_worktree_path, worktree_rev],
108 stderr=subprocess.STDOUT
110 self.log.debug(worktree_output.decode("utf-8"))
111 return git_worktree_path
113 def _update_git_submodules(self, git_worktree_path, version):
114 """If the crypto submodule is present, initialize it.
115 if version.crypto_revision exists, update it to that revision,
116 otherwise update it to the default revision"""
117 update_output = subprocess.check_output(
118 [self.git_command, "submodule", "update", "--init", '--recursive'],
119 cwd=git_worktree_path,
120 stderr=subprocess.STDOUT
122 self.log.debug(update_output.decode("utf-8"))
123 if not (os.path.exists(os.path.join(git_worktree_path, "crypto"))
124 and version.crypto_revision):
127 if version.crypto_repository:
128 fetch_output = subprocess.check_output(
129 [self.git_command, "fetch", version.crypto_repository,
130 version.crypto_revision],
131 cwd=os.path.join(git_worktree_path, "crypto"),
132 stderr=subprocess.STDOUT
134 self.log.debug(fetch_output.decode("utf-8"))
135 crypto_rev = "FETCH_HEAD"
137 crypto_rev = version.crypto_revision
139 checkout_output = subprocess.check_output(
140 [self.git_command, "checkout", crypto_rev],
141 cwd=os.path.join(git_worktree_path, "crypto"),
142 stderr=subprocess.STDOUT
144 self.log.debug(checkout_output.decode("utf-8"))
146 def _build_shared_libraries(self, git_worktree_path, version):
147 """Build the shared libraries in the specified worktree."""
148 my_environment = os.environ.copy()
149 my_environment["CFLAGS"] = "-g -Og"
150 my_environment["SHARED"] = "1"
151 if os.path.exists(os.path.join(git_worktree_path, "crypto")):
152 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
153 make_output = subprocess.check_output(
154 [self.make_command, "lib"],
156 cwd=git_worktree_path,
157 stderr=subprocess.STDOUT
159 self.log.debug(make_output.decode("utf-8"))
160 for root, _dirs, files in os.walk(git_worktree_path):
161 for file in fnmatch.filter(files, "*.so"):
162 version.modules[os.path.splitext(file)[0]] = (
163 os.path.join(root, file)
166 def _get_abi_dumps_from_shared_libraries(self, version):
167 """Generate the ABI dumps for the specified git revision.
168 The shared libraries must have been built and the module paths
169 present in version.modules."""
170 for mbed_module, module_path in list(version.modules.items()):
171 output_path = os.path.join(
172 self.report_dir, "{}-{}-{}.dump".format(
173 mbed_module, version.revision, version.version
180 "-lver", version.revision
182 abi_dump_output = subprocess.check_output(
184 stderr=subprocess.STDOUT
186 self.log.debug(abi_dump_output.decode("utf-8"))
187 version.abi_dumps[mbed_module] = output_path
189 def _cleanup_worktree(self, git_worktree_path):
190 """Remove the specified git worktree."""
191 shutil.rmtree(git_worktree_path)
192 worktree_output = subprocess.check_output(
193 [self.git_command, "worktree", "prune"],
195 stderr=subprocess.STDOUT
197 self.log.debug(worktree_output.decode("utf-8"))
199 def _get_abi_dump_for_ref(self, version):
200 """Generate the ABI dumps for the specified git revision."""
201 git_worktree_path = self._get_clean_worktree_for_git_revision(version)
202 self._update_git_submodules(git_worktree_path, version)
203 self._build_shared_libraries(git_worktree_path, version)
204 self._get_abi_dumps_from_shared_libraries(version)
205 self._cleanup_worktree(git_worktree_path)
207 def _remove_children_with_tag(self, parent, tag):
208 children = parent.getchildren()
209 for child in children:
213 self._remove_children_with_tag(child, tag)
215 def _remove_extra_detail_from_report(self, report_root):
216 for tag in ['test_info', 'test_results', 'problem_summary',
217 'added_symbols', 'affected']:
218 self._remove_children_with_tag(report_root, tag)
220 for report in report_root:
221 for problems in report.getchildren()[:]:
222 if not problems.getchildren():
223 report.remove(problems)
225 def get_abi_compatibility_report(self):
226 """Generate a report of the differences between the reference ABI
227 and the new ABI. ABI dumps from self.old_version and self.new_version
228 must be available."""
229 compatibility_report = ""
230 compliance_return_code = 0
231 shared_modules = list(set(self.old_version.modules.keys()) &
232 set(self.new_version.modules.keys()))
233 for mbed_module in shared_modules:
234 output_path = os.path.join(
235 self.report_dir, "{}-{}-{}.html".format(
236 mbed_module, self.old_version.revision,
237 self.new_version.revision
240 abi_compliance_command = [
241 "abi-compliance-checker",
243 "-old", self.old_version.abi_dumps[mbed_module],
244 "-new", self.new_version.abi_dumps[mbed_module],
246 "-report-path", output_path,
249 abi_compliance_command += ["-skip-symbols", self.skip_file,
250 "-skip-types", self.skip_file]
252 abi_compliance_command += ["-report-format", "xml",
255 subprocess.check_output(
256 abi_compliance_command,
257 stderr=subprocess.STDOUT
259 except subprocess.CalledProcessError as err:
260 if err.returncode == 1:
261 compliance_return_code = 1
264 "Compatibility issues found for {}".format(mbed_module)
266 report_root = ET.fromstring(err.output.decode("utf-8"))
267 self._remove_extra_detail_from_report(report_root)
268 self.log.info(ET.tostring(report_root).decode("utf-8"))
270 self.can_remove_report_dir = False
271 compatibility_report += (
272 "Compatibility issues found for {}, "
273 "for details see {}\n".format(mbed_module, output_path)
278 compatibility_report += (
279 "No compatibility issues for {}\n".format(mbed_module)
281 if not (self.keep_all_reports or self.brief):
282 os.remove(output_path)
283 for version in [self.old_version, self.new_version]:
284 for mbed_module, mbed_module_dump in list(version.abi_dumps.items()):
285 os.remove(mbed_module_dump)
286 if self.can_remove_report_dir:
287 os.rmdir(self.report_dir)
288 self.log.info(compatibility_report)
289 return compliance_return_code
291 def check_for_abi_changes(self):
292 """Generate a report of ABI differences
293 between self.old_rev and self.new_rev."""
294 self.check_repo_path()
295 self.check_abi_tools_are_installed()
296 self._get_abi_dump_for_ref(self.old_version)
297 self._get_abi_dump_for_ref(self.new_version)
298 return self.get_abi_compatibility_report()
303 parser = argparse.ArgumentParser(
305 """This script is a small wrapper around the
306 abi-compliance-checker and abi-dumper tools, applying them
307 to compare the ABI and API of the library files from two
308 different Git revisions within an Mbed TLS repository.
309 The results of the comparison are either formatted as HTML and
310 stored at a configurable location, or are given as a brief list
311 of problems. Returns 0 on success, 1 on ABI/API non-compliance,
312 and 2 if there is an error while running the script.
313 Note: must be run from Mbed TLS root."""
317 "-v", "--verbose", action="store_true",
318 help="set verbosity level",
321 "-r", "--report-dir", type=str, default="reports",
322 help="directory where reports are stored, default is reports",
325 "-k", "--keep-all-reports", action="store_true",
326 help="keep all reports, even if there are no compatibility issues",
329 "-o", "--old-rev", type=str, help="revision for old version.",
333 "-or", "--old-repo", type=str, help="repository for old version."
336 "-oc", "--old-crypto-rev", type=str,
337 help="revision for old crypto submodule."
340 "-ocr", "--old-crypto-repo", type=str,
341 help="repository for old crypto submodule."
344 "-n", "--new-rev", type=str, help="revision for new version",
348 "-nr", "--new-repo", type=str, help="repository for new version."
351 "-nc", "--new-crypto-rev", type=str,
352 help="revision for new crypto version"
355 "-ncr", "--new-crypto-repo", type=str,
356 help="repository for new crypto submodule."
359 "-s", "--skip-file", type=str,
360 help="path to file containing symbols and types to skip"
363 "-b", "--brief", action="store_true",
364 help="output only the list of issues to stdout, instead of a full report",
366 abi_args = parser.parse_args()
367 if os.path.isfile(abi_args.report_dir):
368 print(("Error: {} is not a directory".format(abi_args.report_dir)))
370 old_version = SimpleNamespace(
372 repository=abi_args.old_repo,
373 revision=abi_args.old_rev,
374 crypto_repository=abi_args.old_crypto_repo,
375 crypto_revision=abi_args.old_crypto_rev,
379 new_version = SimpleNamespace(
381 repository=abi_args.new_repo,
382 revision=abi_args.new_rev,
383 crypto_repository=abi_args.new_crypto_repo,
384 crypto_revision=abi_args.new_crypto_rev,
388 configuration = SimpleNamespace(
389 verbose=abi_args.verbose,
390 report_dir=abi_args.report_dir,
391 keep_all_reports=abi_args.keep_all_reports,
392 brief=abi_args.brief,
393 skip_file=abi_args.skip_file
395 abi_check = AbiChecker(old_version, new_version, configuration)
396 return_code = abi_check.check_for_abi_changes()
397 sys.exit(return_code)
398 except Exception: # pylint: disable=broad-except
399 # Print the backtrace and exit explicitly so as to exit with
401 traceback.print_exc()
405 if __name__ == "__main__":