blob: 60200a3ad49e4cd08bb3916ab0b5cd9567d9ee49 [file] [log] [blame]
Doug Zongker75f17362009-12-08 13:46:44 -08001#!/usr/bin/env python
2#
3# Copyright (C) 2009 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Check the signatures of all APKs in a target_files .zip file. With
19-c, compare the signatures of each package to the ones in a separate
20target_files (usually a previously distributed build for the same
21device) and flag any changes.
22
23Usage: check_target_file_signatures [flags] target_files
24
25 -c (--compare_with) <other_target_files>
26 Look for compatibility problems between the two sets of target
27 files (eg., packages whose keys have changed).
28
29 -l (--local_cert_dirs) <dir,dir,...>
30 Comma-separated list of top-level directories to scan for
31 .x509.pem files. Defaults to "vendor,build". Where cert files
32 can be found that match APK signatures, the filename will be
33 printed as the cert name, otherwise a hash of the cert plus its
34 subject string will be printed instead.
35
36 -t (--text)
37 Dump the certificate information for both packages in comparison
38 mode (this output is normally suppressed).
39
40"""
41
Tao Baoa67e12d2017-12-07 10:33:34 -080042from __future__ import print_function
43
Tao Baobadceb22019-03-15 09:33:43 -070044import logging
Tao Bao767543a2018-03-01 10:09:07 -080045import os
Tao Baoa67e12d2017-12-07 10:33:34 -080046import os.path
Tao Bao767543a2018-03-01 10:09:07 -080047import re
48import subprocess
Doug Zongker75f17362009-12-08 13:46:44 -080049import sys
Tao Bao767543a2018-03-01 10:09:07 -080050import zipfile
51
52import common
Doug Zongker75f17362009-12-08 13:46:44 -080053
Doug Zongkercf6d5a92014-02-18 10:57:07 -080054if sys.hexversion < 0x02070000:
Tao Baoa67e12d2017-12-07 10:33:34 -080055 print("Python 2.7 or newer is required.", file=sys.stderr)
Doug Zongker75f17362009-12-08 13:46:44 -080056 sys.exit(1)
57
Doug Zongker75f17362009-12-08 13:46:44 -080058
Tao Baobadceb22019-03-15 09:33:43 -070059logger = logging.getLogger(__name__)
60
Tao Baod32e78f2018-01-17 10:08:48 -080061# Work around a bug in Python's zipfile module that prevents opening of zipfiles
62# if any entry has an extra field of between 1 and 3 bytes (which is common with
63# zipaligned APKs). This overrides the ZipInfo._decodeExtra() method (which
64# contains the bug) with an empty version (since we don't need to decode the
65# extra field anyway).
66# Issue #14315: https://bugs.python.org/issue14315, fixed in Python 2.7.8 and
67# Python 3.5.0 alpha 1.
Doug Zongker75f17362009-12-08 13:46:44 -080068class MyZipInfo(zipfile.ZipInfo):
69 def _decodeExtra(self):
70 pass
Tao Baoa67e12d2017-12-07 10:33:34 -080071
Doug Zongker75f17362009-12-08 13:46:44 -080072zipfile.ZipInfo = MyZipInfo
73
Tao Baoa67e12d2017-12-07 10:33:34 -080074
Doug Zongker75f17362009-12-08 13:46:44 -080075OPTIONS = common.OPTIONS
76
77OPTIONS.text = False
78OPTIONS.compare_with = None
79OPTIONS.local_cert_dirs = ("vendor", "build")
80
81PROBLEMS = []
82PROBLEM_PREFIX = []
83
Tao Baoa67e12d2017-12-07 10:33:34 -080084
Doug Zongker75f17362009-12-08 13:46:44 -080085def AddProblem(msg):
86 PROBLEMS.append(" ".join(PROBLEM_PREFIX) + " " + msg)
Tao Baoa67e12d2017-12-07 10:33:34 -080087
88
Doug Zongker75f17362009-12-08 13:46:44 -080089def Push(msg):
90 PROBLEM_PREFIX.append(msg)
Tao Baoa67e12d2017-12-07 10:33:34 -080091
92
Doug Zongker75f17362009-12-08 13:46:44 -080093def Pop():
94 PROBLEM_PREFIX.pop()
95
96
97def Banner(msg):
Tao Baoa67e12d2017-12-07 10:33:34 -080098 print("-" * 70)
99 print(" ", msg)
100 print("-" * 70)
Doug Zongker75f17362009-12-08 13:46:44 -0800101
102
103def GetCertSubject(cert):
104 p = common.Run(["openssl", "x509", "-inform", "DER", "-text"],
105 stdin=subprocess.PIPE,
Tao Baoa67e12d2017-12-07 10:33:34 -0800106 stdout=subprocess.PIPE,
107 universal_newlines=False)
Doug Zongker75f17362009-12-08 13:46:44 -0800108 out, err = p.communicate(cert)
109 if err and not err.strip():
110 return "(error reading cert subject)"
Tao Baoa67e12d2017-12-07 10:33:34 -0800111 for line in out.decode().split("\n"):
Doug Zongker75f17362009-12-08 13:46:44 -0800112 line = line.strip()
113 if line.startswith("Subject:"):
114 return line[8:].strip()
115 return "(unknown cert subject)"
116
117
118class CertDB(object):
Tao Baoa67e12d2017-12-07 10:33:34 -0800119
Doug Zongker75f17362009-12-08 13:46:44 -0800120 def __init__(self):
121 self.certs = {}
122
123 def Add(self, cert, name=None):
124 if cert in self.certs:
125 if name:
126 self.certs[cert] = self.certs[cert] + "," + name
127 else:
128 if name is None:
Doug Zongker6ae53812011-01-27 10:20:27 -0800129 name = "unknown cert %s (%s)" % (common.sha1(cert).hexdigest()[:12],
Doug Zongker75f17362009-12-08 13:46:44 -0800130 GetCertSubject(cert))
131 self.certs[cert] = name
132
133 def Get(self, cert):
134 """Return the name for a given cert."""
135 return self.certs.get(cert, None)
136
137 def FindLocalCerts(self):
138 to_load = []
139 for top in OPTIONS.local_cert_dirs:
Dan Albert8b72aef2015-03-23 19:13:21 -0700140 for dirpath, _, filenames in os.walk(top):
Doug Zongker75f17362009-12-08 13:46:44 -0800141 certs = [os.path.join(dirpath, i)
142 for i in filenames if i.endswith(".x509.pem")]
143 if certs:
144 to_load.extend(certs)
145
146 for i in to_load:
Tao Baoa67e12d2017-12-07 10:33:34 -0800147 with open(i) as f:
148 cert = common.ParseCertificate(f.read())
Doug Zongker75f17362009-12-08 13:46:44 -0800149 name, _ = os.path.splitext(i)
150 name, _ = os.path.splitext(name)
151 self.Add(cert, name)
152
Tao Baoa67e12d2017-12-07 10:33:34 -0800153
Doug Zongker75f17362009-12-08 13:46:44 -0800154ALL_CERTS = CertDB()
155
156
Doug Zongker75f17362009-12-08 13:46:44 -0800157def CertFromPKCS7(data, filename):
158 """Read the cert out of a PKCS#7-format file (which is what is
159 stored in a signed .apk)."""
160 Push(filename + ":")
161 try:
162 p = common.Run(["openssl", "pkcs7",
163 "-inform", "DER",
164 "-outform", "PEM",
165 "-print_certs"],
166 stdin=subprocess.PIPE,
Tao Baoa67e12d2017-12-07 10:33:34 -0800167 stdout=subprocess.PIPE,
168 universal_newlines=False)
Doug Zongker75f17362009-12-08 13:46:44 -0800169 out, err = p.communicate(data)
170 if err and not err.strip():
Tao Baoa67e12d2017-12-07 10:33:34 -0800171 AddProblem("error reading cert:\n" + err.decode())
Doug Zongker75f17362009-12-08 13:46:44 -0800172 return None
173
Tao Baoa67e12d2017-12-07 10:33:34 -0800174 cert = common.ParseCertificate(out.decode())
Doug Zongker75f17362009-12-08 13:46:44 -0800175 if not cert:
176 AddProblem("error parsing cert output")
177 return None
178 return cert
179 finally:
180 Pop()
181
182
183class APK(object):
Tao Bao359862d2019-03-20 12:24:58 -0700184
Doug Zongker75f17362009-12-08 13:46:44 -0800185 def __init__(self, full_filename, filename):
186 self.filename = filename
Dan Albert8b72aef2015-03-23 19:13:21 -0700187 self.certs = None
188 self.shared_uid = None
189 self.package = None
190
Doug Zongker75f17362009-12-08 13:46:44 -0800191 Push(filename+":")
192 try:
Doug Zongkera5f534d2011-11-11 09:51:37 -0800193 self.RecordCerts(full_filename)
Doug Zongker75f17362009-12-08 13:46:44 -0800194 self.ReadManifest(full_filename)
195 finally:
196 Pop()
197
Doug Zongkera5f534d2011-11-11 09:51:37 -0800198 def RecordCerts(self, full_filename):
199 out = set()
Tao Baoa67e12d2017-12-07 10:33:34 -0800200 with zipfile.ZipFile(full_filename) as apk:
Doug Zongker75f17362009-12-08 13:46:44 -0800201 pkcs7 = None
202 for info in apk.infolist():
Tao Baoa67e12d2017-12-07 10:33:34 -0800203 filename = info.filename
204 if (filename.startswith("META-INF/") and
205 info.filename.endswith((".DSA", ".RSA"))):
206 pkcs7 = apk.read(filename)
207 cert = CertFromPKCS7(pkcs7, filename)
Doug Zongkera5f534d2011-11-11 09:51:37 -0800208 out.add(cert)
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700209 ALL_CERTS.Add(cert)
Doug Zongker75f17362009-12-08 13:46:44 -0800210 if not pkcs7:
211 AddProblem("no signature")
Tao Baoa67e12d2017-12-07 10:33:34 -0800212
213 self.certs = frozenset(out)
Doug Zongker75f17362009-12-08 13:46:44 -0800214
215 def ReadManifest(self, full_filename):
216 p = common.Run(["aapt", "dump", "xmltree", full_filename,
217 "AndroidManifest.xml"],
218 stdout=subprocess.PIPE)
219 manifest, err = p.communicate()
220 if err:
221 AddProblem("failed to read manifest")
222 return
223
224 self.shared_uid = None
225 self.package = None
226
227 for line in manifest.split("\n"):
228 line = line.strip()
Dan Albert8b72aef2015-03-23 19:13:21 -0700229 m = re.search(r'A: (\S*?)(?:\(0x[0-9a-f]+\))?="(.*?)" \(Raw', line)
Doug Zongker75f17362009-12-08 13:46:44 -0800230 if m:
231 name = m.group(1)
232 if name == "android:sharedUserId":
233 if self.shared_uid is not None:
234 AddProblem("multiple sharedUserId declarations")
235 self.shared_uid = m.group(2)
236 elif name == "package":
237 if self.package is not None:
238 AddProblem("multiple package declarations")
239 self.package = m.group(2)
240
241 if self.package is None:
242 AddProblem("no package declaration")
243
244
245class TargetFiles(object):
246 def __init__(self):
247 self.max_pkg_len = 30
248 self.max_fn_len = 20
Dan Albert8b72aef2015-03-23 19:13:21 -0700249 self.apks = None
250 self.apks_by_basename = None
251 self.certmap = None
Doug Zongker75f17362009-12-08 13:46:44 -0800252
253 def LoadZipFile(self, filename):
Narayan Kamatha07bf042017-08-14 14:49:21 +0100254 # First read the APK certs file to figure out whether there are compressed
255 # APKs in the archive. If we do have compressed APKs in the archive, then we
256 # must decompress them individually before we perform any analysis.
257
258 # This is the list of wildcards of files we extract from |filename|.
Tao Bao359862d2019-03-20 12:24:58 -0700259 apk_extensions = ['*.apk', '*.apex']
Narayan Kamatha07bf042017-08-14 14:49:21 +0100260
Tao Baoa67e12d2017-12-07 10:33:34 -0800261 with zipfile.ZipFile(filename) as input_zip:
262 self.certmap, compressed_extension = common.ReadApkCerts(input_zip)
Narayan Kamatha07bf042017-08-14 14:49:21 +0100263 if compressed_extension:
Tao Bao359862d2019-03-20 12:24:58 -0700264 apk_extensions.append('*.apk' + compressed_extension)
Narayan Kamatha07bf042017-08-14 14:49:21 +0100265
Tao Baodba59ee2018-01-09 13:21:02 -0800266 d = common.UnzipTemp(filename, apk_extensions)
Tao Bao767543a2018-03-01 10:09:07 -0800267 self.apks = {}
268 self.apks_by_basename = {}
269 for dirpath, _, filenames in os.walk(d):
270 for fn in filenames:
271 # Decompress compressed APKs before we begin processing them.
272 if compressed_extension and fn.endswith(compressed_extension):
273 # First strip the compressed extension from the file.
274 uncompressed_fn = fn[:-len(compressed_extension)]
Narayan Kamatha07bf042017-08-14 14:49:21 +0100275
Tao Bao767543a2018-03-01 10:09:07 -0800276 # Decompress the compressed file to the output file.
277 common.Gunzip(os.path.join(dirpath, fn),
278 os.path.join(dirpath, uncompressed_fn))
Narayan Kamatha07bf042017-08-14 14:49:21 +0100279
Tao Bao767543a2018-03-01 10:09:07 -0800280 # Finally, delete the compressed file and use the uncompressed file
281 # for further processing. Note that the deletion is not strictly
282 # required, but is done here to ensure that we're not using too much
283 # space in the temporary directory.
284 os.remove(os.path.join(dirpath, fn))
285 fn = uncompressed_fn
Narayan Kamatha07bf042017-08-14 14:49:21 +0100286
Tao Bao359862d2019-03-20 12:24:58 -0700287 if fn.endswith(('.apk', '.apex')):
Tao Bao767543a2018-03-01 10:09:07 -0800288 fullname = os.path.join(dirpath, fn)
289 displayname = fullname[len(d)+1:]
290 apk = APK(fullname, displayname)
291 self.apks[apk.filename] = apk
292 self.apks_by_basename[os.path.basename(apk.filename)] = apk
Narayan Kamatha07bf042017-08-14 14:49:21 +0100293
Tao Bao767543a2018-03-01 10:09:07 -0800294 self.max_pkg_len = max(self.max_pkg_len, len(apk.package))
295 self.max_fn_len = max(self.max_fn_len, len(apk.filename))
Doug Zongker75f17362009-12-08 13:46:44 -0800296
297 def CheckSharedUids(self):
298 """Look for any instances where packages signed with different
299 certs request the same sharedUserId."""
300 apks_by_uid = {}
Tao Baoa67e12d2017-12-07 10:33:34 -0800301 for apk in self.apks.values():
Doug Zongker75f17362009-12-08 13:46:44 -0800302 if apk.shared_uid:
303 apks_by_uid.setdefault(apk.shared_uid, []).append(apk)
304
Tao Bao767543a2018-03-01 10:09:07 -0800305 for uid in sorted(apks_by_uid):
Doug Zongker75f17362009-12-08 13:46:44 -0800306 apks = apks_by_uid[uid]
307 for apk in apks[1:]:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700308 if apk.certs != apks[0].certs:
Doug Zongker75f17362009-12-08 13:46:44 -0800309 break
310 else:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700311 # all packages have the same set of certs; this uid is fine.
Doug Zongker75f17362009-12-08 13:46:44 -0800312 continue
313
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700314 AddProblem("different cert sets for packages with uid %s" % (uid,))
Doug Zongker75f17362009-12-08 13:46:44 -0800315
Tao Baoa67e12d2017-12-07 10:33:34 -0800316 print("uid %s is shared by packages with different cert sets:" % (uid,))
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700317 for apk in apks:
Tao Baoa67e12d2017-12-07 10:33:34 -0800318 print("%-*s [%s]" % (self.max_pkg_len, apk.package, apk.filename))
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700319 for cert in apk.certs:
Tao Baoa67e12d2017-12-07 10:33:34 -0800320 print(" ", ALL_CERTS.Get(cert))
321 print()
Doug Zongker75f17362009-12-08 13:46:44 -0800322
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800323 def CheckExternalSignatures(self):
Tao Baoa67e12d2017-12-07 10:33:34 -0800324 for apk_filename, certname in self.certmap.items():
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800325 if certname == "EXTERNAL":
326 # Apps marked EXTERNAL should be signed with the test key
327 # during development, then manually re-signed after
328 # predexopting. Consider it an error if this app is now
329 # signed with any key that is present in our tree.
330 apk = self.apks_by_basename[apk_filename]
331 name = ALL_CERTS.Get(apk.cert)
332 if not name.startswith("unknown "):
333 Push(apk.filename)
334 AddProblem("hasn't been signed with EXTERNAL cert")
335 Pop()
336
Doug Zongker75f17362009-12-08 13:46:44 -0800337 def PrintCerts(self):
338 """Display a table of packages grouped by cert."""
339 by_cert = {}
Tao Baoa67e12d2017-12-07 10:33:34 -0800340 for apk in self.apks.values():
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700341 for cert in apk.certs:
342 by_cert.setdefault(cert, []).append((apk.package, apk))
Doug Zongker75f17362009-12-08 13:46:44 -0800343
Tao Baoa67e12d2017-12-07 10:33:34 -0800344 order = [(-len(v), k) for (k, v) in by_cert.items()]
Doug Zongker75f17362009-12-08 13:46:44 -0800345 order.sort()
346
347 for _, cert in order:
Tao Baoa67e12d2017-12-07 10:33:34 -0800348 print("%s:" % (ALL_CERTS.Get(cert),))
Doug Zongker75f17362009-12-08 13:46:44 -0800349 apks = by_cert[cert]
350 apks.sort()
351 for _, apk in apks:
352 if apk.shared_uid:
Tao Baoa67e12d2017-12-07 10:33:34 -0800353 print(" %-*s %-*s [%s]" % (self.max_fn_len, apk.filename,
Doug Zongker75f17362009-12-08 13:46:44 -0800354 self.max_pkg_len, apk.package,
Tao Baoa67e12d2017-12-07 10:33:34 -0800355 apk.shared_uid))
Doug Zongker75f17362009-12-08 13:46:44 -0800356 else:
Tao Baoa67e12d2017-12-07 10:33:34 -0800357 print(" %-*s %s" % (self.max_fn_len, apk.filename, apk.package))
358 print()
Doug Zongker75f17362009-12-08 13:46:44 -0800359
360 def CompareWith(self, other):
361 """Look for instances where a given package that exists in both
362 self and other have different certs."""
363
Dan Albert8b72aef2015-03-23 19:13:21 -0700364 all_apks = set(self.apks.keys())
365 all_apks.update(other.apks.keys())
Doug Zongker75f17362009-12-08 13:46:44 -0800366
367 max_pkg_len = max(self.max_pkg_len, other.max_pkg_len)
368
369 by_certpair = {}
370
Tao Bao726b7f32015-06-03 17:31:34 -0700371 for i in all_apks:
Doug Zongker75f17362009-12-08 13:46:44 -0800372 if i in self.apks:
373 if i in other.apks:
Doug Zongker278c9782011-11-09 10:32:23 -0800374 # in both; should have same set of certs
375 if self.apks[i].certs != other.apks[i].certs:
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700376 by_certpair.setdefault((other.apks[i].certs,
377 self.apks[i].certs), []).append(i)
Doug Zongker75f17362009-12-08 13:46:44 -0800378 else:
Tao Baoa67e12d2017-12-07 10:33:34 -0800379 print("%s [%s]: new APK (not in comparison target_files)" % (
380 i, self.apks[i].filename))
Doug Zongker75f17362009-12-08 13:46:44 -0800381 else:
382 if i in other.apks:
Tao Baoa67e12d2017-12-07 10:33:34 -0800383 print("%s [%s]: removed APK (only in comparison target_files)" % (
384 i, other.apks[i].filename))
Doug Zongker75f17362009-12-08 13:46:44 -0800385
386 if by_certpair:
387 AddProblem("some APKs changed certs")
388 Banner("APK signing differences")
389 for (old, new), packages in sorted(by_certpair.items()):
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700390 for i, o in enumerate(old):
391 if i == 0:
Tao Baoa67e12d2017-12-07 10:33:34 -0800392 print("was", ALL_CERTS.Get(o))
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700393 else:
Tao Baoa67e12d2017-12-07 10:33:34 -0800394 print(" ", ALL_CERTS.Get(o))
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700395 for i, n in enumerate(new):
396 if i == 0:
Tao Baoa67e12d2017-12-07 10:33:34 -0800397 print("now", ALL_CERTS.Get(n))
Doug Zongkerb40a58e2011-09-29 13:22:57 -0700398 else:
Tao Baoa67e12d2017-12-07 10:33:34 -0800399 print(" ", ALL_CERTS.Get(n))
Doug Zongker75f17362009-12-08 13:46:44 -0800400 for i in sorted(packages):
401 old_fn = other.apks[i].filename
402 new_fn = self.apks[i].filename
403 if old_fn == new_fn:
Tao Baoa67e12d2017-12-07 10:33:34 -0800404 print(" %-*s [%s]" % (max_pkg_len, i, old_fn))
Doug Zongker75f17362009-12-08 13:46:44 -0800405 else:
Tao Baoa67e12d2017-12-07 10:33:34 -0800406 print(" %-*s [was: %s; now: %s]" % (max_pkg_len, i,
407 old_fn, new_fn))
408 print()
Doug Zongker75f17362009-12-08 13:46:44 -0800409
410
411def main(argv):
412 def option_handler(o, a):
413 if o in ("-c", "--compare_with"):
414 OPTIONS.compare_with = a
415 elif o in ("-l", "--local_cert_dirs"):
416 OPTIONS.local_cert_dirs = [i.strip() for i in a.split(",")]
417 elif o in ("-t", "--text"):
418 OPTIONS.text = True
419 else:
420 return False
421 return True
422
423 args = common.ParseOptions(argv, __doc__,
424 extra_opts="c:l:t",
425 extra_long_opts=["compare_with=",
426 "local_cert_dirs="],
427 extra_option_handler=option_handler)
428
429 if len(args) != 1:
430 common.Usage(__doc__)
431 sys.exit(1)
432
Tao Baobadceb22019-03-15 09:33:43 -0700433 common.InitLogging()
434
Doug Zongker75f17362009-12-08 13:46:44 -0800435 ALL_CERTS.FindLocalCerts()
436
437 Push("input target_files:")
438 try:
439 target_files = TargetFiles()
440 target_files.LoadZipFile(args[0])
441 finally:
442 Pop()
443
444 compare_files = None
445 if OPTIONS.compare_with:
446 Push("comparison target_files:")
447 try:
448 compare_files = TargetFiles()
449 compare_files.LoadZipFile(OPTIONS.compare_with)
450 finally:
451 Pop()
452
453 if OPTIONS.text or not compare_files:
454 Banner("target files")
455 target_files.PrintCerts()
456 target_files.CheckSharedUids()
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800457 target_files.CheckExternalSignatures()
Doug Zongker75f17362009-12-08 13:46:44 -0800458 if compare_files:
459 if OPTIONS.text:
460 Banner("comparison files")
461 compare_files.PrintCerts()
462 target_files.CompareWith(compare_files)
463
464 if PROBLEMS:
Tao Baoa67e12d2017-12-07 10:33:34 -0800465 print("%d problem(s) found:\n" % (len(PROBLEMS),))
Doug Zongker75f17362009-12-08 13:46:44 -0800466 for p in PROBLEMS:
Tao Baoa67e12d2017-12-07 10:33:34 -0800467 print(p)
Doug Zongker75f17362009-12-08 13:46:44 -0800468 return 1
469
470 return 0
471
472
473if __name__ == '__main__':
474 try:
475 r = main(sys.argv[1:])
476 sys.exit(r)
Dan Albert8b72aef2015-03-23 19:13:21 -0700477 except common.ExternalError as e:
Tao Baoa67e12d2017-12-07 10:33:34 -0800478 print("\n ERROR: %s\n" % (e,))
Doug Zongker75f17362009-12-08 13:46:44 -0800479 sys.exit(1)
Tao Bao767543a2018-03-01 10:09:07 -0800480 finally:
481 common.Cleanup()