Fix build error with scons-4.4.0 version which is based on python3
[platform/upstream/iotivity.git] / extlibs / mbedtls / mbedtls / 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 list(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', '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         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
290
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()
299
300
301 def run_main():
302     try:
303         parser = argparse.ArgumentParser(
304             description=(
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."""
314             )
315         )
316         parser.add_argument(
317             "-v", "--verbose", action="store_true",
318             help="set verbosity level",
319         )
320         parser.add_argument(
321             "-r", "--report-dir", type=str, default="reports",
322             help="directory where reports are stored, default is reports",
323         )
324         parser.add_argument(
325             "-k", "--keep-all-reports", action="store_true",
326             help="keep all reports, even if there are no compatibility issues",
327         )
328         parser.add_argument(
329             "-o", "--old-rev", type=str, help="revision for old version.",
330             required=True,
331         )
332         parser.add_argument(
333             "-or", "--old-repo", type=str, help="repository for old version."
334         )
335         parser.add_argument(
336             "-oc", "--old-crypto-rev", type=str,
337             help="revision for old crypto submodule."
338         )
339         parser.add_argument(
340             "-ocr", "--old-crypto-repo", type=str,
341             help="repository for old crypto submodule."
342         )
343         parser.add_argument(
344             "-n", "--new-rev", type=str, help="revision for new version",
345             required=True,
346         )
347         parser.add_argument(
348             "-nr", "--new-repo", type=str, help="repository for new version."
349         )
350         parser.add_argument(
351             "-nc", "--new-crypto-rev", type=str,
352             help="revision for new crypto version"
353         )
354         parser.add_argument(
355             "-ncr", "--new-crypto-repo", type=str,
356             help="repository for new crypto submodule."
357         )
358         parser.add_argument(
359             "-s", "--skip-file", type=str,
360             help="path to file containing symbols and types to skip"
361         )
362         parser.add_argument(
363             "-b", "--brief", action="store_true",
364             help="output only the list of issues to stdout, instead of a full report",
365         )
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)))
369             parser.exit()
370         old_version = SimpleNamespace(
371             version="old",
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,
376             abi_dumps={},
377             modules={}
378         )
379         new_version = SimpleNamespace(
380             version="new",
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,
385             abi_dumps={},
386             modules={}
387         )
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
394         )
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
400         # status 2, not 1.
401         traceback.print_exc()
402         sys.exit(2)
403
404
405 if __name__ == "__main__":
406     run_main()