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 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', 'removed_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 os.remove(self.old_version.abi_dumps[mbed_module])
284 os.remove(self.new_version.abi_dumps[mbed_module])
285 if self.can_remove_report_dir:
286 os.rmdir(self.report_dir)
287 self.log.info(compatibility_report)
288 return compliance_return_code
290 def check_for_abi_changes(self):
291 """Generate a report of ABI differences
292 between self.old_rev and self.new_rev."""
293 self.check_repo_path()
294 self.check_abi_tools_are_installed()
295 self._get_abi_dump_for_ref(self.old_version)
296 self._get_abi_dump_for_ref(self.new_version)
297 return self.get_abi_compatibility_report()
302 parser = argparse.ArgumentParser(
304 """This script is a small wrapper around the
305 abi-compliance-checker and abi-dumper tools, applying them
306 to compare the ABI and API of the library files from two
307 different Git revisions within an Mbed TLS repository.
308 The results of the comparison are either formatted as HTML and
309 stored at a configurable location, or are given as a brief list
310 of problems. Returns 0 on success, 1 on ABI/API non-compliance,
311 and 2 if there is an error while running the script.
312 Note: must be run from Mbed TLS root."""
316 "-v", "--verbose", action="store_true",
317 help="set verbosity level",
320 "-r", "--report-dir", type=str, default="reports",
321 help="directory where reports are stored, default is reports",
324 "-k", "--keep-all-reports", action="store_true",
325 help="keep all reports, even if there are no compatibility issues",
328 "-o", "--old-rev", type=str, help="revision for old version.",
332 "-or", "--old-repo", type=str, help="repository for old version."
335 "-oc", "--old-crypto-rev", type=str,
336 help="revision for old crypto submodule."
339 "-ocr", "--old-crypto-repo", type=str,
340 help="repository for old crypto submodule."
343 "-n", "--new-rev", type=str, help="revision for new version",
347 "-nr", "--new-repo", type=str, help="repository for new version."
350 "-nc", "--new-crypto-rev", type=str,
351 help="revision for new crypto version"
354 "-ncr", "--new-crypto-repo", type=str,
355 help="repository for new crypto submodule."
358 "-s", "--skip-file", type=str,
359 help="path to file containing symbols and types to skip"
362 "-b", "--brief", action="store_true",
363 help="output only the list of issues to stdout, instead of a full report",
365 abi_args = parser.parse_args()
366 if os.path.isfile(abi_args.report_dir):
367 print("Error: {} is not a directory".format(abi_args.report_dir))
369 old_version = SimpleNamespace(
371 repository=abi_args.old_repo,
372 revision=abi_args.old_rev,
373 crypto_repository=abi_args.old_crypto_repo,
374 crypto_revision=abi_args.old_crypto_rev,
378 new_version = SimpleNamespace(
380 repository=abi_args.new_repo,
381 revision=abi_args.new_rev,
382 crypto_repository=abi_args.new_crypto_repo,
383 crypto_revision=abi_args.new_crypto_rev,
387 configuration = SimpleNamespace(
388 verbose=abi_args.verbose,
389 report_dir=abi_args.report_dir,
390 keep_all_reports=abi_args.keep_all_reports,
391 brief=abi_args.brief,
392 skip_file=abi_args.skip_file
394 abi_check = AbiChecker(old_version, new_version, configuration)
395 return_code = abi_check.check_for_abi_changes()
396 sys.exit(return_code)
397 except Exception: # pylint: disable=broad-except
398 # Print the backtrace and exit explicitly so as to exit with
400 traceback.print_exc()
404 if __name__ == "__main__":