blob: cc866789994123850246efe74e1a1af2bc0f1e2c [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
Darryl Green7381bea2019-03-05 16:25:38 +000031class RepoVersion(object):
32
33 def __init__(self, version, repository, revision,
34 crypto_repository, crypto_revision):
35 """Class containing details for a particular revision.
36
37 version: either 'old' or 'new'
38 repository: repository for git revision
39 revision: git revision for comparison
40 crypto_repository: repository for git revision of crypto submodule
41 crypto_revision: git revision of crypto submodule
42 """
43 self.version = version
44 self.repository = repository
45 self.revision = revision
46 self.crypto_repository = crypto_repository
47 self.crypto_revision = crypto_revision
48 self.abi_dumps = {}
49 self.modules = {}
50
51
Darryl Green7c2dd582018-03-01 14:53:49 +000052class AbiChecker(object):
Gilles Peskine9df17632019-02-25 20:36:52 +010053 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000054
Darryl Green7381bea2019-03-05 16:25:38 +000055 def __init__(self, old_version, new_version, report_dir,
56 keep_all_reports, brief, skip_file=None):
Gilles Peskine9df17632019-02-25 20:36:52 +010057 """Instantiate the API/ABI checker.
58
Darryl Green7381bea2019-03-05 16:25:38 +000059 old_version: RepoVersion containing details to compare against
60 new_version: RepoVersion containing details to check
Gilles Peskine9df17632019-02-25 20:36:52 +010061 report_dir: directory for output files
Gilles Peskine9df17632019-02-25 20:36:52 +010062 keep_all_reports: if false, delete old reports
Darryl Green0da45782019-02-21 13:09:26 +000063 brief: if true, output shorter report to stdout
Darryl Green668063b2019-02-20 15:01:56 +000064 skip_file: path to file containing symbols and types to skip
Gilles Peskine9df17632019-02-25 20:36:52 +010065 """
Darryl Green7c2dd582018-03-01 14:53:49 +000066 self.repo_path = "."
67 self.log = None
68 self.setup_logger()
69 self.report_dir = os.path.abspath(report_dir)
70 self.keep_all_reports = keep_all_reports
Darryl Green131e24b2019-02-25 17:01:55 +000071 self.can_remove_report_dir = not (os.path.isdir(self.report_dir) or
72 keep_all_reports)
Darryl Green7381bea2019-03-05 16:25:38 +000073 self.old_version = old_version
74 self.new_version = new_version
Darryl Green668063b2019-02-20 15:01:56 +000075 self.skip_file = skip_file
Darryl Green0da45782019-02-21 13:09:26 +000076 self.brief = brief
Darryl Green7c2dd582018-03-01 14:53:49 +000077 self.git_command = "git"
78 self.make_command = "make"
79
Gilles Peskine9df17632019-02-25 20:36:52 +010080 @staticmethod
81 def check_repo_path():
Darryl Greena6f430f2018-03-15 10:12:06 +000082 current_dir = os.path.realpath('.')
83 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
84 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000085 raise Exception("Must be run from Mbed TLS root")
86
87 def setup_logger(self):
88 self.log = logging.getLogger()
89 self.log.setLevel(logging.INFO)
90 self.log.addHandler(logging.StreamHandler())
91
Gilles Peskine9df17632019-02-25 20:36:52 +010092 @staticmethod
93 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +000094 for command in ["abi-dumper", "abi-compliance-checker"]:
95 if not shutil.which(command):
96 raise Exception("{} not installed, aborting".format(command))
97
Darryl Green7381bea2019-03-05 16:25:38 +000098 def get_clean_worktree_for_git_revision(self, version):
99 """Make a separate worktree with version.revision checked out.
Gilles Peskine9df17632019-02-25 20:36:52 +0100100 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000101 git_worktree_path = tempfile.mkdtemp()
Darryl Green7381bea2019-03-05 16:25:38 +0000102 if version.repository:
Darryl Green5a301f02019-02-19 16:59:33 +0000103 self.log.info(
104 "Checking out git worktree for revision {} from {}".format(
Darryl Green7381bea2019-03-05 16:25:38 +0000105 version.revision, version.repository
Darryl Green5a301f02019-02-19 16:59:33 +0000106 )
107 )
108 fetch_process = subprocess.Popen(
Darryl Green7381bea2019-03-05 16:25:38 +0000109 [self.git_command, "fetch",
110 version.repository, version.revision],
Darryl Green5a301f02019-02-19 16:59:33 +0000111 cwd=self.repo_path,
112 stdout=subprocess.PIPE,
113 stderr=subprocess.STDOUT
114 )
115 fetch_output, _ = fetch_process.communicate()
116 self.log.info(fetch_output.decode("utf-8"))
117 if fetch_process.returncode != 0:
118 raise Exception("Fetching revision failed, aborting")
119 worktree_rev = "FETCH_HEAD"
120 else:
Darryl Green7381bea2019-03-05 16:25:38 +0000121 self.log.info("Checking out git worktree for revision {}".format(
122 version.revision
123 ))
124 worktree_rev = version.revision
Darryl Green7c2dd582018-03-01 14:53:49 +0000125 worktree_process = subprocess.Popen(
Darryl Green5a301f02019-02-19 16:59:33 +0000126 [self.git_command, "worktree", "add", "--detach",
127 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000128 cwd=self.repo_path,
129 stdout=subprocess.PIPE,
130 stderr=subprocess.STDOUT
131 )
132 worktree_output, _ = worktree_process.communicate()
133 self.log.info(worktree_output.decode("utf-8"))
134 if worktree_process.returncode != 0:
135 raise Exception("Checking out worktree failed, aborting")
136 return git_worktree_path
137
Darryl Green7381bea2019-03-05 16:25:38 +0000138 def update_git_submodules(self, git_worktree_path, version):
Jaeden Amero4cd4b4b2018-11-02 16:35:09 +0000139 process = subprocess.Popen(
140 [self.git_command, "submodule", "update", "--init", '--recursive'],
141 cwd=git_worktree_path,
142 stdout=subprocess.PIPE,
143 stderr=subprocess.STDOUT
144 )
145 output, _ = process.communicate()
146 self.log.info(output.decode("utf-8"))
147 if process.returncode != 0:
148 raise Exception("git submodule update failed, aborting")
Darryl Green0478a322019-03-05 15:23:25 +0000149 if not (os.path.exists(os.path.join(git_worktree_path, "crypto"))
Darryl Green7381bea2019-03-05 16:25:38 +0000150 and version.crypto_revision):
Darryl Green0478a322019-03-05 15:23:25 +0000151 return
152
Darryl Green7381bea2019-03-05 16:25:38 +0000153 if version.crypto_repository:
Darryl Green0478a322019-03-05 15:23:25 +0000154 shutil.rmtree(os.path.join(git_worktree_path, "crypto"))
155 clone_process = subprocess.Popen(
Darryl Green7381bea2019-03-05 16:25:38 +0000156 [self.git_command, "clone", version.crypto_repository,
157 "--branch", version.crypto_revision, "crypto"],
Darryl Green0478a322019-03-05 15:23:25 +0000158 cwd=git_worktree_path,
159 stdout=subprocess.PIPE,
160 stderr=subprocess.STDOUT
161 )
162 clone_output, _ = clone_process.communicate()
163 self.log.info(clone_output.decode("utf-8"))
164 if clone_process.returncode != 0:
165 raise Exception("git clone failed, aborting")
166 else:
167 checkout_process = subprocess.Popen(
Darryl Green7381bea2019-03-05 16:25:38 +0000168 [self.git_command, "checkout", version.crypto_revision],
Darryl Green0478a322019-03-05 15:23:25 +0000169 cwd=os.path.join(git_worktree_path, "crypto"),
170 stdout=subprocess.PIPE,
171 stderr=subprocess.STDOUT
172 )
173 checkout_output, _ = checkout_process.communicate()
174 self.log.info(checkout_output.decode("utf-8"))
175 if checkout_process.returncode != 0:
176 raise Exception("git checkout failed, aborting")
Jaeden Amero4cd4b4b2018-11-02 16:35:09 +0000177
Darryl Greende118092019-02-27 16:53:40 +0000178 def build_shared_libraries(self, git_worktree_path, version):
Gilles Peskine9df17632019-02-25 20:36:52 +0100179 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000180 my_environment = os.environ.copy()
181 my_environment["CFLAGS"] = "-g -Og"
182 my_environment["SHARED"] = "1"
Darryl Greenae5d66c2019-02-25 11:35:05 +0000183 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Green7c2dd582018-03-01 14:53:49 +0000184 make_process = subprocess.Popen(
Darryl Greenc8e6ad42019-02-28 11:52:39 +0000185 [self.make_command, "lib"],
Darryl Green7c2dd582018-03-01 14:53:49 +0000186 env=my_environment,
187 cwd=git_worktree_path,
188 stdout=subprocess.PIPE,
189 stderr=subprocess.STDOUT
190 )
191 make_output, _ = make_process.communicate()
192 self.log.info(make_output.decode("utf-8"))
Darryl Greenae5d66c2019-02-25 11:35:05 +0000193 for root, dirs, files in os.walk(git_worktree_path):
194 for file in fnmatch.filter(files, "*.so"):
Darryl Green7381bea2019-03-05 16:25:38 +0000195 version.modules[os.path.splitext(file)[0]] = (
Darryl Greende118092019-02-27 16:53:40 +0000196 os.path.join(root, file)
Darryl Greenae5d66c2019-02-25 11:35:05 +0000197 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000198 if make_process.returncode != 0:
199 raise Exception("make failed, aborting")
200
Darryl Green7381bea2019-03-05 16:25:38 +0000201 def get_abi_dumps_from_shared_libraries(self, git_worktree_path,
Darryl Greende118092019-02-27 16:53:40 +0000202 version):
Gilles Peskine9df17632019-02-25 20:36:52 +0100203 """Generate the ABI dumps for the specified git revision.
204 It must be checked out in git_worktree_path and the shared libraries
205 must have been built."""
Darryl Green7381bea2019-03-05 16:25:38 +0000206 for mbed_module, module_path in version.modules.items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000207 output_path = os.path.join(
Darryl Green7381bea2019-03-05 16:25:38 +0000208 self.report_dir, version.version, "{}-{}.dump".format(
209 mbed_module, version.revision
Darryl Greende118092019-02-27 16:53:40 +0000210 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000211 )
212 abi_dump_command = [
213 "abi-dumper",
Darryl Greenae5d66c2019-02-25 11:35:05 +0000214 module_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000215 "-o", output_path,
Darryl Green7381bea2019-03-05 16:25:38 +0000216 "-lver", version.revision
Darryl Green7c2dd582018-03-01 14:53:49 +0000217 ]
218 abi_dump_process = subprocess.Popen(
219 abi_dump_command,
220 stdout=subprocess.PIPE,
221 stderr=subprocess.STDOUT
222 )
223 abi_dump_output, _ = abi_dump_process.communicate()
224 self.log.info(abi_dump_output.decode("utf-8"))
225 if abi_dump_process.returncode != 0:
226 raise Exception("abi-dumper failed, aborting")
Darryl Green7381bea2019-03-05 16:25:38 +0000227 version.abi_dumps[mbed_module] = output_path
Darryl Green7c2dd582018-03-01 14:53:49 +0000228
229 def cleanup_worktree(self, git_worktree_path):
Gilles Peskine9df17632019-02-25 20:36:52 +0100230 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000231 shutil.rmtree(git_worktree_path)
232 worktree_process = subprocess.Popen(
233 [self.git_command, "worktree", "prune"],
234 cwd=self.repo_path,
235 stdout=subprocess.PIPE,
236 stderr=subprocess.STDOUT
237 )
238 worktree_output, _ = worktree_process.communicate()
239 self.log.info(worktree_output.decode("utf-8"))
240 if worktree_process.returncode != 0:
241 raise Exception("Worktree cleanup failed, aborting")
242
Darryl Green7381bea2019-03-05 16:25:38 +0000243 def get_abi_dump_for_ref(self, version):
Gilles Peskine9df17632019-02-25 20:36:52 +0100244 """Generate the ABI dumps for the specified git revision."""
Darryl Green7381bea2019-03-05 16:25:38 +0000245 git_worktree_path = self.get_clean_worktree_for_git_revision(version)
246 self.update_git_submodules(git_worktree_path, version)
Darryl Greende118092019-02-27 16:53:40 +0000247 self.build_shared_libraries(git_worktree_path, version)
Darryl Green7381bea2019-03-05 16:25:38 +0000248 self.get_abi_dumps_from_shared_libraries(git_worktree_path, version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000249 self.cleanup_worktree(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000250
Darryl Green0da45782019-02-21 13:09:26 +0000251 def remove_children_with_tag(self, parent, tag):
252 children = parent.getchildren()
253 for child in children:
254 if child.tag == tag:
255 parent.remove(child)
256 else:
257 self.remove_children_with_tag(child, tag)
258
259 def remove_extra_detail_from_report(self, report_root):
260 for tag in ['test_info', 'test_results', 'problem_summary',
261 'added_symbols', 'removed_symbols', 'affected']:
262 self.remove_children_with_tag(report_root, tag)
263
264 for report in report_root:
265 for problems in report.getchildren()[:]:
266 if not problems.getchildren():
267 report.remove(problems)
268
Darryl Green7c2dd582018-03-01 14:53:49 +0000269 def get_abi_compatibility_report(self):
Gilles Peskine9df17632019-02-25 20:36:52 +0100270 """Generate a report of the differences between the reference ABI
271 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
272 be available."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000273 compatibility_report = ""
274 compliance_return_code = 0
Darryl Green7381bea2019-03-05 16:25:38 +0000275 shared_modules = list(set(self.old_version.modules.keys()) &
276 set(self.new_version.modules.keys()))
Darryl Greende118092019-02-27 16:53:40 +0000277 for mbed_module in shared_modules:
Darryl Green7c2dd582018-03-01 14:53:49 +0000278 output_path = os.path.join(
279 self.report_dir, "{}-{}-{}.html".format(
Darryl Green7381bea2019-03-05 16:25:38 +0000280 mbed_module, self.old_version.revision,
281 self.new_version.revision
Darryl Green7c2dd582018-03-01 14:53:49 +0000282 )
283 )
284 abi_compliance_command = [
285 "abi-compliance-checker",
286 "-l", mbed_module,
Darryl Green7381bea2019-03-05 16:25:38 +0000287 "-old", self.old_version.abi_dumps[mbed_module],
288 "-new", self.new_version.abi_dumps[mbed_module],
Darryl Green7c2dd582018-03-01 14:53:49 +0000289 "-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 )
Darryl Green7381bea2019-03-05 16:25:38 +0000330 os.remove(self.old_version.abi_dumps[mbed_module])
331 os.remove(self.new_version.abi_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 Green7381bea2019-03-05 16:25:38 +0000342 self.get_abi_dump_for_ref(self.old_version)
343 self.get_abi_dump_for_ref(self.new_version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000344 return self.get_abi_compatibility_report()
345
346
347def run_main():
348 try:
349 parser = argparse.ArgumentParser(
350 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100351 """This script is a small wrapper around the
352 abi-compliance-checker and abi-dumper tools, applying them
353 to compare the ABI and API of the library files from two
354 different Git revisions within an Mbed TLS repository.
Darryl Green0da45782019-02-21 13:09:26 +0000355 The results of the comparison are either formatted as HTML and
Darryl Green7c0e0522019-03-05 15:21:32 +0000356 stored at a configurable location, or are given as a brief list
357 of problems. Returns 0 on success, 1 on ABI/API non-compliance,
358 and 2 if there is an error while running the script.
359 Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000360 )
361 )
362 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100363 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000364 help="directory where reports are stored, default is reports",
365 )
366 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100367 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000368 help="keep all reports, even if there are no compatibility issues",
369 )
370 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000371 "-o", "--old-rev", type=str, help="revision for old version.",
372 required=True,
Darryl Green7c2dd582018-03-01 14:53:49 +0000373 )
374 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000375 "-or", "--old-repo", type=str, help="repository for old version."
Darryl Greenae5d66c2019-02-25 11:35:05 +0000376 )
377 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000378 "-oc", "--old-crypto-rev", type=str,
379 help="revision for old crypto submodule."
Darryl Green7c2dd582018-03-01 14:53:49 +0000380 )
Darryl Green668063b2019-02-20 15:01:56 +0000381 parser.add_argument(
Darryl Green06c51d02019-03-01 09:54:44 +0000382 "-ocr", "--old-crypto-repo", type=str,
383 help="repository for old crypto submodule."
384 )
385 parser.add_argument(
386 "-n", "--new-rev", type=str, help="revision for new version",
387 required=True,
388 )
389 parser.add_argument(
390 "-nr", "--new-repo", type=str, help="repository for new version."
391 )
392 parser.add_argument(
393 "-nc", "--new-crypto-rev", type=str,
394 help="revision for new crypto version"
395 )
396 parser.add_argument(
397 "-ncr", "--new-crypto-repo", type=str,
398 help="repository for new crypto submodule."
Darryl Greenae5d66c2019-02-25 11:35:05 +0000399 )
400 parser.add_argument(
Darryl Green668063b2019-02-20 15:01:56 +0000401 "-s", "--skip-file", type=str,
402 help="path to file containing symbols and types to skip"
403 )
Darryl Green0da45782019-02-21 13:09:26 +0000404 parser.add_argument(
405 "-b", "--brief", action="store_true",
406 help="output only the list of issues to stdout, instead of a full report",
407 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000408 abi_args = parser.parse_args()
Darryl Green7381bea2019-03-05 16:25:38 +0000409 old_version = RepoVersion("old", abi_args.old_repo, abi_args.old_rev,
410 abi_args.old_crypto_repo, abi_args.old_crypto_rev)
411 new_version = RepoVersion("new", abi_args.new_repo, abi_args.new_rev,
412 abi_args.new_crypto_repo, abi_args.new_crypto_rev)
Darryl Green7c2dd582018-03-01 14:53:49 +0000413 abi_check = AbiChecker(
Darryl Green7381bea2019-03-05 16:25:38 +0000414 old_version, new_version, abi_args.report_dir,
415 abi_args.keep_all_reports, abi_args.brief, abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000416 )
417 return_code = abi_check.check_for_abi_changes()
418 sys.exit(return_code)
Gilles Peskineafd19dd2019-02-25 21:39:42 +0100419 except Exception: # pylint: disable=broad-except
420 # Print the backtrace and exit explicitly so as to exit with
421 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000422 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000423 sys.exit(2)
424
425
426if __name__ == "__main__":
427 run_main()