Fix for x86_64 build fail
[platform/upstream/connectedhomeip.git] / third_party / mbedtls / repo / scripts / abi_check.py
1 #!/usr/bin/env python3
2 """
3 This file is part of Mbed TLS (https://tls.mbed.org)
4
5 Copyright (c) 2018, Arm Limited, All Rights Reserved
6
7 Purpose
8
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.
16 """
17
18 import os
19 import sys
20 import traceback
21 import shutil
22 import subprocess
23 import argparse
24 import logging
25 import tempfile
26 import fnmatch
27 from types import SimpleNamespace
28
29 import xml.etree.ElementTree as ET
30
31
32 class AbiChecker(object):
33     """API and ABI checker."""
34
35     def __init__(self, old_version, new_version, configuration):
36         """Instantiate the API/ABI checker.
37
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
44         """
45         self.repo_path = "."
46         self.log = None
47         self.verbose = configuration.verbose
48         self._setup_logger()
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"
59
60     @staticmethod
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")
66
67     def _setup_logger(self):
68         self.log = logging.getLogger()
69         if self.verbose:
70             self.log.setLevel(logging.DEBUG)
71         else:
72             self.log.setLevel(logging.INFO)
73         self.log.addHandler(logging.StreamHandler())
74
75     @staticmethod
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))
80
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:
86             self.log.debug(
87                 "Checking out git worktree for revision {} from {}".format(
88                     version.revision, version.repository
89                 )
90             )
91             fetch_output = subprocess.check_output(
92                 [self.git_command, "fetch",
93                  version.repository, version.revision],
94                 cwd=self.repo_path,
95                 stderr=subprocess.STDOUT
96             )
97             self.log.debug(fetch_output.decode("utf-8"))
98             worktree_rev = "FETCH_HEAD"
99         else:
100             self.log.debug("Checking out git worktree for revision {}".format(
101                 version.revision
102             ))
103             worktree_rev = version.revision
104         worktree_output = subprocess.check_output(
105             [self.git_command, "worktree", "add", "--detach",
106              git_worktree_path, worktree_rev],
107             cwd=self.repo_path,
108             stderr=subprocess.STDOUT
109         )
110         self.log.debug(worktree_output.decode("utf-8"))
111         return git_worktree_path
112
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
121         )
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):
125             return
126
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
133             )
134             self.log.debug(fetch_output.decode("utf-8"))
135             crypto_rev = "FETCH_HEAD"
136         else:
137             crypto_rev = version.crypto_revision
138
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
143         )
144         self.log.debug(checkout_output.decode("utf-8"))
145
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"],
155             env=my_environment,
156             cwd=git_worktree_path,
157             stderr=subprocess.STDOUT
158         )
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)
164                 )
165
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
174                 )
175             )
176             abi_dump_command = [
177                 "abi-dumper",
178                 module_path,
179                 "-o", output_path,
180                 "-lver", version.revision
181             ]
182             abi_dump_output = subprocess.check_output(
183                 abi_dump_command,
184                 stderr=subprocess.STDOUT
185             )
186             self.log.debug(abi_dump_output.decode("utf-8"))
187             version.abi_dumps[mbed_module] = output_path
188
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"],
194             cwd=self.repo_path,
195             stderr=subprocess.STDOUT
196         )
197         self.log.debug(worktree_output.decode("utf-8"))
198
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)
206
207     def _remove_children_with_tag(self, parent, tag):
208         children = parent.getchildren()
209         for child in children:
210             if child.tag == tag:
211                 parent.remove(child)
212             else:
213                 self._remove_children_with_tag(child, tag)
214
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)
219
220         for report in report_root:
221             for problems in report.getchildren()[:]:
222                 if not problems.getchildren():
223                     report.remove(problems)
224
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
238                 )
239             )
240             abi_compliance_command = [
241                 "abi-compliance-checker",
242                 "-l", mbed_module,
243                 "-old", self.old_version.abi_dumps[mbed_module],
244                 "-new", self.new_version.abi_dumps[mbed_module],
245                 "-strict",
246                 "-report-path", output_path,
247             ]
248             if self.skip_file:
249                 abi_compliance_command += ["-skip-symbols", self.skip_file,
250                                            "-skip-types", self.skip_file]
251             if self.brief:
252                 abi_compliance_command += ["-report-format", "xml",
253                                            "-stdout"]
254             try:
255                 subprocess.check_output(
256                     abi_compliance_command,
257                     stderr=subprocess.STDOUT
258                 )
259             except subprocess.CalledProcessError as err:
260                 if err.returncode == 1:
261                     compliance_return_code = 1
262                     if self.brief:
263                         self.log.info(
264                             "Compatibility issues found for {}".format(mbed_module)
265                         )
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"))
269                     else:
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)
274                         )
275                 else:
276                     raise err
277             else:
278                 compatibility_report += (
279                     "No compatibility issues for {}\n".format(mbed_module)
280                 )
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
289
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()
298
299
300 def run_main():
301     try:
302         parser = argparse.ArgumentParser(
303             description=(
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."""
313             )
314         )
315         parser.add_argument(
316             "-v", "--verbose", action="store_true",
317             help="set verbosity level",
318         )
319         parser.add_argument(
320             "-r", "--report-dir", type=str, default="reports",
321             help="directory where reports are stored, default is reports",
322         )
323         parser.add_argument(
324             "-k", "--keep-all-reports", action="store_true",
325             help="keep all reports, even if there are no compatibility issues",
326         )
327         parser.add_argument(
328             "-o", "--old-rev", type=str, help="revision for old version.",
329             required=True,
330         )
331         parser.add_argument(
332             "-or", "--old-repo", type=str, help="repository for old version."
333         )
334         parser.add_argument(
335             "-oc", "--old-crypto-rev", type=str,
336             help="revision for old crypto submodule."
337         )
338         parser.add_argument(
339             "-ocr", "--old-crypto-repo", type=str,
340             help="repository for old crypto submodule."
341         )
342         parser.add_argument(
343             "-n", "--new-rev", type=str, help="revision for new version",
344             required=True,
345         )
346         parser.add_argument(
347             "-nr", "--new-repo", type=str, help="repository for new version."
348         )
349         parser.add_argument(
350             "-nc", "--new-crypto-rev", type=str,
351             help="revision for new crypto version"
352         )
353         parser.add_argument(
354             "-ncr", "--new-crypto-repo", type=str,
355             help="repository for new crypto submodule."
356         )
357         parser.add_argument(
358             "-s", "--skip-file", type=str,
359             help="path to file containing symbols and types to skip"
360         )
361         parser.add_argument(
362             "-b", "--brief", action="store_true",
363             help="output only the list of issues to stdout, instead of a full report",
364         )
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))
368             parser.exit()
369         old_version = SimpleNamespace(
370             version="old",
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,
375             abi_dumps={},
376             modules={}
377         )
378         new_version = SimpleNamespace(
379             version="new",
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,
384             abi_dumps={},
385             modules={}
386         )
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
393         )
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
399         # status 2, not 1.
400         traceback.print_exc()
401         sys.exit(2)
402
403
404 if __name__ == "__main__":
405     run_main()