blob: 7e6d7fcfddb4255f7808c25e39690a5990ef64af [file] [log] [blame]
Darryl Green7c2dd582018-03-01 14:53:49 +00001#!/usr/bin/env python3
Darryl Green78696802018-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 Green0da45782019-02-21 13:09:26 +000012The results of the comparison are either formatted as HTML and stored at
Darryl Green7c0e0522019-03-05 15:21:32 +000013a configurable location, or are given as a brief list of problems.
Darryl Green0da45782019-02-21 13:09:26 +000014Returns 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 Green78696802018-04-06 11:23:22 +010016"""
Darryl Green7c2dd582018-03-01 14:53:49 +000017
18import os
19import sys
20import traceback
21import shutil
22import subprocess
23import argparse
24import logging
25import tempfile
Darryl Greenae5d66c2019-02-25 11:35:05 +000026import fnmatch
Darryl Green7c2dd582018-03-01 14:53:49 +000027
Darryl Green0da45782019-02-21 13:09:26 +000028import xml.etree.ElementTree as ET
29
Darryl Green7c2dd582018-03-01 14:53:49 +000030
31class AbiChecker(object):
Gilles Peskine9df17632019-02-25 20:36:52 +010032 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000033
Darryl Greenae5d66c2019-02-25 11:35:05 +000034 def __init__(self, report_dir, old_repo, old_rev, old_crypto_rev,
Darryl Green879f2502019-02-27 17:33:31 +000035 old_crypto_repo, new_repo, new_rev, new_crypto_rev,
36 new_crypto_repo, keep_all_reports, brief, skip_file=None):
Gilles Peskine9df17632019-02-25 20:36:52 +010037 """Instantiate the API/ABI checker.
38
39 report_dir: directory for output files
Darryl Green5a301f02019-02-19 16:59:33 +000040 old_repo: repository for git revision to compare against
Gilles Peskine9df17632019-02-25 20:36:52 +010041 old_rev: reference git revision to compare against
Darryl Greenae5d66c2019-02-25 11:35:05 +000042 old_crypto_rev: reference git revision for old crypto submodule
Darryl Green879f2502019-02-27 17:33:31 +000043 old_crypto_repo: repository for git revision for old crypto submodule
Darryl Green5a301f02019-02-19 16:59:33 +000044 new_repo: repository for git revision to check
Gilles Peskine9df17632019-02-25 20:36:52 +010045 new_rev: git revision to check
Darryl Greenae5d66c2019-02-25 11:35:05 +000046 new_crypto_rev: reference git revision for new crypto submodule
Darryl Green879f2502019-02-27 17:33:31 +000047 new_crypto_repo: repository for git revision for new crypto submodule
Gilles Peskine9df17632019-02-25 20:36:52 +010048 keep_all_reports: if false, delete old reports
Darryl Green0da45782019-02-21 13:09:26 +000049 brief: if true, output shorter report to stdout
Darryl Green668063b2019-02-20 15:01:56 +000050 skip_file: path to file containing symbols and types to skip
Gilles Peskine9df17632019-02-25 20:36:52 +010051 """
Darryl Green7c2dd582018-03-01 14:53:49 +000052 self.repo_path = "."
53 self.log = None
54 self.setup_logger()
55 self.report_dir = os.path.abspath(report_dir)
56 self.keep_all_reports = keep_all_reports
Darryl Green131e24b2019-02-25 17:01:55 +000057 self.can_remove_report_dir = not (os.path.isdir(self.report_dir) or
58 keep_all_reports)
Darryl Green5a301f02019-02-19 16:59:33 +000059 self.old_repo = old_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000060 self.old_rev = old_rev
Darryl Greenae5d66c2019-02-25 11:35:05 +000061 self.old_crypto_rev = old_crypto_rev
Darryl Green879f2502019-02-27 17:33:31 +000062 self.old_crypto_repo = old_crypto_repo
Darryl Green5a301f02019-02-19 16:59:33 +000063 self.new_repo = new_repo
Darryl Green7c2dd582018-03-01 14:53:49 +000064 self.new_rev = new_rev
Darryl Greenae5d66c2019-02-25 11:35:05 +000065 self.new_crypto_rev = new_crypto_rev
Darryl Green879f2502019-02-27 17:33:31 +000066 self.new_crypto_repo = new_crypto_repo
Darryl Green668063b2019-02-20 15:01:56 +000067 self.skip_file = skip_file
Darryl Green0da45782019-02-21 13:09:26 +000068 self.brief = brief
Darryl Greende118092019-02-27 16:53:40 +000069 self.mbedtls_modules = {"old": {}, "new": {}}
Darryl Green7c2dd582018-03-01 14:53:49 +000070 self.old_dumps = {}
71 self.new_dumps = {}
72 self.git_command = "git"
73 self.make_command = "make"
74
Gilles Peskine9df17632019-02-25 20:36:52 +010075 @staticmethod
76 def check_repo_path():
Darryl Greena6f430f2018-03-15 10:12:06 +000077 current_dir = os.path.realpath('.')
78 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
79 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000080 raise Exception("Must be run from Mbed TLS root")
81
82 def setup_logger(self):
83 self.log = logging.getLogger()
84 self.log.setLevel(logging.INFO)
85 self.log.addHandler(logging.StreamHandler())
86
Gilles Peskine9df17632019-02-25 20:36:52 +010087 @staticmethod
88 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +000089 for command in ["abi-dumper", "abi-compliance-checker"]:
90 if not shutil.which(command):
91 raise Exception("{} not installed, aborting".format(command))
92
Darryl Green5a301f02019-02-19 16:59:33 +000093 def get_clean_worktree_for_git_revision(self, remote_repo, git_rev):
Gilles Peskine9df17632019-02-25 20:36:52 +010094 """Make a separate worktree with git_rev checked out.
95 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +000096 git_worktree_path = tempfile.mkdtemp()
Darryl Green5a301f02019-02-19 16:59:33 +000097 if remote_repo:
98 self.log.info(
99 "Checking out git worktree for revision {} from {}".format(
100 git_rev, remote_repo
101 )
102 )
103 fetch_process = subprocess.Popen(
104 [self.git_command, "fetch", remote_repo, git_rev],
105 cwd=self.repo_path,
106 stdout=subprocess.PIPE,
107 stderr=subprocess.STDOUT
108 )
109 fetch_output, _ = fetch_process.communicate()
110 self.log.info(fetch_output.decode("utf-8"))
111 if fetch_process.returncode != 0:
112 raise Exception("Fetching revision failed, aborting")
113 worktree_rev = "FETCH_HEAD"
114 else:
115 self.log.info(
116 "Checking out git worktree for revision {}".format(git_rev)
117 )
118 worktree_rev = git_rev
Darryl Green7c2dd582018-03-01 14:53:49 +0000119 worktree_process = subprocess.Popen(
Darryl Green5a301f02019-02-19 16:59:33 +0000120 [self.git_command, "worktree", "add", "--detach",
121 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000122 cwd=self.repo_path,
123 stdout=subprocess.PIPE,
124 stderr=subprocess.STDOUT
125 )
126 worktree_output, _ = worktree_process.communicate()
127 self.log.info(worktree_output.decode("utf-8"))
128 if worktree_process.returncode != 0:
129 raise Exception("Checking out worktree failed, aborting")
130 return git_worktree_path
131
Darryl Green879f2502019-02-27 17:33:31 +0000132 def update_git_submodules(self, git_worktree_path, crypto_repo,
133 crypto_rev):
Jaeden Amero4cd4b4b2018-11-02 16:35:09 +0000134 process = subprocess.Popen(
135 [self.git_command, "submodule", "update", "--init", '--recursive'],
136 cwd=git_worktree_path,
137 stdout=subprocess.PIPE,
138 stderr=subprocess.STDOUT
139 )
140 output, _ = process.communicate()
141 self.log.info(output.decode("utf-8"))
142 if process.returncode != 0:
143 raise Exception("git submodule update failed, aborting")
Darryl Greenae5d66c2019-02-25 11:35:05 +0000144 if (os.path.exists(os.path.join(git_worktree_path, "crypto"))
145 and crypto_rev):
Darryl Green879f2502019-02-27 17:33:31 +0000146 if crypto_repo:
147 shutil.rmtree(os.path.join(git_worktree_path, "crypto"))
148 clone_process = subprocess.Popen(
149 [self.git_command, "clone", crypto_repo,
150 "--branch", crypto_rev, "crypto"],
151 cwd=git_worktree_path,
152 stdout=subprocess.PIPE,
153 stderr=subprocess.STDOUT
154 )
155 clone_output, _ = clone_process.communicate()
156 self.log.info(clone_output.decode("utf-8"))
157 if clone_process.returncode != 0:
158 raise Exception("git clone failed, aborting")
159 else:
160 checkout_process = subprocess.Popen(
161 [self.git_command, "checkout", crypto_rev],
162 cwd=os.path.join(git_worktree_path, "crypto"),
163 stdout=subprocess.PIPE,
164 stderr=subprocess.STDOUT
165 )
166 checkout_output, _ = checkout_process.communicate()
167 self.log.info(checkout_output.decode("utf-8"))
168 if checkout_process.returncode != 0:
169 raise Exception("git checkout failed, aborting")
Jaeden Amero4cd4b4b2018-11-02 16:35:09 +0000170
Darryl Greende118092019-02-27 16:53:40 +0000171 def build_shared_libraries(self, git_worktree_path, version):
Gilles Peskine9df17632019-02-25 20:36:52 +0100172 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000173 my_environment = os.environ.copy()
174 my_environment["CFLAGS"] = "-g -Og"
175 my_environment["SHARED"] = "1"
Darryl Greenae5d66c2019-02-25 11:35:05 +0000176 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Green7c2dd582018-03-01 14:53:49 +0000177 make_process = subprocess.Popen(
Darryl Greenc8e6ad42019-02-28 11:52:39 +0000178 [self.make_command, "lib"],
Darryl Green7c2dd582018-03-01 14:53:49 +0000179 env=my_environment,
180 cwd=git_worktree_path,
181 stdout=subprocess.PIPE,
182 stderr=subprocess.STDOUT
183 )
184 make_output, _ = make_process.communicate()
185 self.log.info(make_output.decode("utf-8"))
Darryl Greenae5d66c2019-02-25 11:35:05 +0000186 for root, dirs, files in os.walk(git_worktree_path):
187 for file in fnmatch.filter(files, "*.so"):
Darryl Greende118092019-02-27 16:53:40 +0000188 self.mbedtls_modules[version][os.path.splitext(file)[0]] = (
189 os.path.join(root, file)
Darryl Greenae5d66c2019-02-25 11:35:05 +0000190 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000191 if make_process.returncode != 0:
192 raise Exception("make failed, aborting")
193
Darryl Greende118092019-02-27 16:53:40 +0000194 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path,
195 version):
Gilles Peskine9df17632019-02-25 20:36:52 +0100196 """Generate the ABI dumps for the specified git revision.
197 It must be checked out in git_worktree_path and the shared libraries
198 must have been built."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000199 abi_dumps = {}
Darryl Greende118092019-02-27 16:53:40 +0000200 for mbed_module, module_path in self.mbedtls_modules[version].items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000201 output_path = os.path.join(
Darryl Greende118092019-02-27 16:53:40 +0000202 self.report_dir, version, "{}-{}.dump".format(
203 mbed_module, git_ref
204 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000205 )
206 abi_dump_command = [
207 "abi-dumper",
Darryl Greenae5d66c2019-02-25 11:35:05 +0000208 module_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000209 "-o", output_path,
210 "-lver", git_ref
211 ]
212 abi_dump_process = subprocess.Popen(
213 abi_dump_command,
214 stdout=subprocess.PIPE,
215 stderr=subprocess.STDOUT
216 )
217 abi_dump_output, _ = abi_dump_process.communicate()
218 self.log.info(abi_dump_output.decode("utf-8"))
219 if abi_dump_process.returncode != 0:
220 raise Exception("abi-dumper failed, aborting")
221 abi_dumps[mbed_module] = output_path
222 return abi_dumps
223
224 def cleanup_worktree(self, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +0100225 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000226 shutil.rmtree(git_worktree_path)
227 worktree_process = subprocess.Popen(
228 [self.git_command, "worktree", "prune"],
229 cwd=self.repo_path,
230 stdout=subprocess.PIPE,
231 stderr=subprocess.STDOUT
232 )
233 worktree_output, _ = worktree_process.communicate()
234 self.log.info(worktree_output.decode("utf-8"))
235 if worktree_process.returncode != 0:
236 raise Exception("Worktree cleanup failed, aborting")
237
Darryl Green879f2502019-02-27 17:33:31 +0000238 def get_abi_dump_for_ref(self, remote_repo, git_rev, crypto_repo,
239 crypto_rev, version):
Gilles Peskine9df17632019-02-25 20:36:52 +0100240 """Generate the ABI dumps for the specified git revision."""
Darryl Green5a301f02019-02-19 16:59:33 +0000241 git_worktree_path = self.get_clean_worktree_for_git_revision(
242 remote_repo, git_rev
243 )
Darryl Green879f2502019-02-27 17:33:31 +0000244 self.update_git_submodules(git_worktree_path, crypto_repo, crypto_rev)
Darryl Greende118092019-02-27 16:53:40 +0000245 self.build_shared_libraries(git_worktree_path, version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000246 abi_dumps = self.get_abi_dumps_from_shared_libraries(
Darryl Greende118092019-02-27 16:53:40 +0000247 git_rev, git_worktree_path, version
Darryl Green7c2dd582018-03-01 14:53:49 +0000248 )
249 self.cleanup_worktree(git_worktree_path)
250 return abi_dumps
251
Darryl Green0da45782019-02-21 13:09:26 +0000252 def remove_children_with_tag(self, parent, tag):
253 children = parent.getchildren()
254 for child in children:
255 if child.tag == tag:
256 parent.remove(child)
257 else:
258 self.remove_children_with_tag(child, tag)
259
260 def remove_extra_detail_from_report(self, report_root):
261 for tag in ['test_info', 'test_results', 'problem_summary',
262 'added_symbols', 'removed_symbols', 'affected']:
263 self.remove_children_with_tag(report_root, tag)
264
265 for report in report_root:
266 for problems in report.getchildren()[:]:
267 if not problems.getchildren():
268 report.remove(problems)
269
Darryl Green7c2dd582018-03-01 14:53:49 +0000270 def get_abi_compatibility_report(self):
Gilles Peskine9df17632019-02-25 20:36:52 +0100271 """Generate a report of the differences between the reference ABI
272 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
273 be available."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000274 compatibility_report = ""
275 compliance_return_code = 0
Darryl Greende118092019-02-27 16:53:40 +0000276 shared_modules = list(set(self.mbedtls_modules["old"].keys()) &
277 set(self.mbedtls_modules["new"].keys()))
278 for mbed_module in shared_modules:
Darryl Green7c2dd582018-03-01 14:53:49 +0000279 output_path = os.path.join(
280 self.report_dir, "{}-{}-{}.html".format(
281 mbed_module, self.old_rev, self.new_rev
282 )
283 )
284 abi_compliance_command = [
285 "abi-compliance-checker",
286 "-l", mbed_module,
287 "-old", self.old_dumps[mbed_module],
288 "-new", self.new_dumps[mbed_module],
289 "-strict",
Darryl Green0da45782019-02-21 13:09:26 +0000290 "-report-path", output_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000291 ]
Darryl Green668063b2019-02-20 15:01:56 +0000292 if self.skip_file:
293 abi_compliance_command += ["-skip-symbols", self.skip_file,
294 "-skip-types", self.skip_file]
Darryl Green0da45782019-02-21 13:09:26 +0000295 if self.brief:
296 abi_compliance_command += ["-report-format", "xml",
297 "-stdout"]
Darryl Green7c2dd582018-03-01 14:53:49 +0000298 abi_compliance_process = subprocess.Popen(
299 abi_compliance_command,
300 stdout=subprocess.PIPE,
301 stderr=subprocess.STDOUT
302 )
303 abi_compliance_output, _ = abi_compliance_process.communicate()
Darryl Green7c2dd582018-03-01 14:53:49 +0000304 if abi_compliance_process.returncode == 0:
305 compatibility_report += (
306 "No compatibility issues for {}\n".format(mbed_module)
307 )
Darryl Green0da45782019-02-21 13:09:26 +0000308 if not (self.keep_all_reports or self.brief):
Darryl Green7c2dd582018-03-01 14:53:49 +0000309 os.remove(output_path)
310 elif abi_compliance_process.returncode == 1:
Darryl Green0da45782019-02-21 13:09:26 +0000311 if self.brief:
312 self.log.info(
313 "Compatibility issues found for {}".format(mbed_module)
314 )
315 report_root = ET.fromstring(abi_compliance_output.decode("utf-8"))
316 self.remove_extra_detail_from_report(report_root)
317 self.log.info(ET.tostring(report_root).decode("utf-8"))
318 else:
319 compliance_return_code = 1
320 self.can_remove_report_dir = False
321 compatibility_report += (
322 "Compatibility issues found for {}, "
323 "for details see {}\n".format(mbed_module, output_path)
324 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000325 else:
326 raise Exception(
327 "abi-compliance-checker failed with a return code of {},"
328 " aborting".format(abi_compliance_process.returncode)
329 )
330 os.remove(self.old_dumps[mbed_module])
331 os.remove(self.new_dumps[mbed_module])
Darryl Green131e24b2019-02-25 17:01:55 +0000332 if self.can_remove_report_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +0000333 os.rmdir(self.report_dir)
334 self.log.info(compatibility_report)
335 return compliance_return_code
336
337 def check_for_abi_changes(self):
Gilles Peskine9df17632019-02-25 20:36:52 +0100338 """Generate a report of ABI differences
339 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000340 self.check_repo_path()
341 self.check_abi_tools_are_installed()
Darryl Greenae5d66c2019-02-25 11:35:05 +0000342 self.old_dumps = self.get_abi_dump_for_ref(self.old_repo, self.old_rev,
Darryl Green879f2502019-02-27 17:33:31 +0000343 self.old_crypto_repo,
Darryl Greende118092019-02-27 16:53:40 +0000344 self.old_crypto_rev, "old")
Darryl Greenae5d66c2019-02-25 11:35:05 +0000345 self.new_dumps = self.get_abi_dump_for_ref(self.new_repo, self.new_rev,
Darryl Green879f2502019-02-27 17:33:31 +0000346 self.new_crypto_repo,
Darryl Greende118092019-02-27 16:53:40 +0000347 self.new_crypto_rev, "new")
Darryl Green7c2dd582018-03-01 14:53:49 +0000348 return self.get_abi_compatibility_report()
349
350
351def run_main():
352 try:
353 parser = argparse.ArgumentParser(
354 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100355 """This script is a small wrapper around the
356 abi-compliance-checker and abi-dumper tools, applying them
357 to compare the ABI and API of the library files from two
358 different Git revisions within an Mbed TLS repository.
Darryl Green0da45782019-02-21 13:09:26 +0000359 The results of the comparison are either formatted as HTML and
Darryl Green7c0e0522019-03-05 15:21:32 +0000360 stored at a configurable location, or are given as a brief list
361 of problems. Returns 0 on success, 1 on ABI/API non-compliance,
362 and 2 if there is an error while running the script.
363 Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000364 )
365 )
366 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100367 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000368 help="directory where reports are stored, default is reports",
369 )
370 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100371 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000372 help="keep all reports, even if there are no compatibility issues",
373 )
374 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000375 "-o", "--old-rev", type=str, help="revision for old version.",
376 required=True,
Darryl Green7c2dd582018-03-01 14:53:49 +0000377 )
378 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000379 "-or", "--old-repo", type=str, help="repository for old version."
Darryl Greenae5d66c2019-02-25 11:35:05 +0000380 )
381 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000382 "-oc", "--old-crypto-rev", type=str,
383 help="revision for old crypto submodule."
Darryl Green7c2dd582018-03-01 14:53:49 +0000384 )
Darryl Green668063b2019-02-20 15:01:56 +0000385 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000386 "-ocr", "--old-crypto-repo", type=str,
387 help="repository for old crypto submodule."
388 )
389 parser.add_argument(
390 "-n", "--new-rev", type=str, help="revision for new version",
391 required=True,
392 )
393 parser.add_argument(
394 "-nr", "--new-repo", type=str, help="repository for new version."
395 )
396 parser.add_argument(
397 "-nc", "--new-crypto-rev", type=str,
398 help="revision for new crypto version"
399 )
400 parser.add_argument(
401 "-ncr", "--new-crypto-repo", type=str,
402 help="repository for new crypto submodule."
Darryl Greenae5d66c2019-02-25 11:35:05 +0000403 )
404 parser.add_argument(
Darryl Green668063b2019-02-20 15:01:56 +0000405 "-s", "--skip-file", type=str,
406 help="path to file containing symbols and types to skip"
407 )
Darryl Green0da45782019-02-21 13:09:26 +0000408 parser.add_argument(
409 "-b", "--brief", action="store_true",
410 help="output only the list of issues to stdout, instead of a full report",
411 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000412 abi_args = parser.parse_args()
413 abi_check = AbiChecker(
Darryl Green06c51d02019-03-01 09:54:44 +0000414 abi_args.report_dir, abi_args.old_repo, abi_args.old_rev,
415 abi_args.old_crypto_rev, abi_args.old_crypto_repo,
416 abi_args.new_repo, abi_args.new_rev, abi_args.new_crypto_rev,
417 abi_args.new_crypto_repo, abi_args.keep_all_reports,
418 abi_args.brief, abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000419 )
420 return_code = abi_check.check_for_abi_changes()
421 sys.exit(return_code)
Gilles Peskineafd19dd2019-02-25 21:39:42 +0100422 except Exception: # pylint: disable=broad-except
423 # Print the backtrace and exit explicitly so as to exit with
424 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000425 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000426 sys.exit(2)
427
428
429if __name__ == "__main__":
430 run_main()