1#!/usr/bin/env python
2
3"""
4CI build script
5(C) 2017,2020 Jack Lloyd
6
7Botan is released under the Simplified BSD License (see license.txt)
8"""
9
10import os
11import platform
12import subprocess
13import sys
14import time
15import tempfile
16import optparse # pylint: disable=deprecated-module
17
18def get_concurrency():
19    def_concurrency = 2
20
21    try:
22        import multiprocessing
23        return multiprocessing.cpu_count()
24    except ImportError:
25        return def_concurrency
26
27def build_targets(target, target_os):
28    if target in ['shared', 'minimized', 'bsi', 'nist']:
29        yield 'shared'
30    elif target in ['static', 'fuzzers', 'baremetal']:
31        yield 'static'
32    elif target_os in ['windows']:
33        yield 'shared'
34    elif target_os in ['ios', 'mingw']:
35        yield 'static'
36    else:
37        yield 'shared'
38        yield 'static'
39
40    yield 'cli'
41    yield 'tests'
42
43    if target in ['coverage']:
44        yield 'bogo_shim'
45
46def determine_flags(target, target_os, target_cpu, target_cc, cc_bin,
47                    ccache, root_dir, pkcs11_lib, use_gdb, disable_werror, extra_cxxflags,
48                    disabled_tests):
49    # pylint: disable=too-many-branches,too-many-statements,too-many-arguments,too-many-locals
50
51    """
52    Return the configure.py flags as well as make/test running prefixes
53    """
54    is_cross_target = target.startswith('cross-')
55
56    if target_os not in ['linux', 'osx', 'windows', 'freebsd']:
57        print('Error unknown OS %s' % (target_os))
58        return (None, None, None)
59
60    if is_cross_target:
61        if target_os == 'osx':
62            target_os = 'ios'
63        elif target == 'cross-win64':
64            target_os = 'mingw'
65        elif target in ['cross-android-arm32', 'cross-android-arm64']:
66            target_os = 'android'
67
68    if target_os == 'windows' and target_cc == 'gcc':
69        target_os = 'mingw'
70
71    if target == 'baremetal':
72        target_os = 'none'
73
74    make_prefix = []
75    test_prefix = []
76    test_cmd = [os.path.join(root_dir, 'botan-test')]
77
78    install_prefix = tempfile.mkdtemp(prefix='botan-install-')
79
80    flags = ['--prefix=%s' % (install_prefix),
81             '--cc=%s' % (target_cc),
82             '--os=%s' % (target_os),
83             '--build-targets=%s' % ','.join(build_targets(target, target_os))]
84
85    if ccache is not None:
86        flags += ['--no-store-vc-rev', '--compiler-cache=%s' % (ccache)]
87
88    if target_os != 'osx' and not disable_werror:
89        flags += ['--werror-mode']
90
91    if target_cpu is not None:
92        flags += ['--cpu=%s' % (target_cpu)]
93
94    for flag in extra_cxxflags:
95        flags += ['--extra-cxxflags=%s' % (flag)]
96
97    if target in ['minimized']:
98        flags += ['--minimized-build', '--enable-modules=system_rng,sha2_32,sha2_64,aes']
99
100    if target in ['bsi', 'nist']:
101        # tls is optional for bsi/nist but add it so verify tests work with these minimized configs
102        flags += ['--module-policy=%s' % (target), '--enable-modules=tls']
103
104    if target == 'docs':
105        flags += ['--with-doxygen', '--with-sphinx', '--with-rst2man']
106        test_cmd = None
107
108    if target == 'cross-win64':
109        # this test compiles under MinGW but fails when run under Wine
110        disabled_tests.append('certstor_system')
111
112    if target == 'coverage':
113        flags += ['--with-coverage-info', '--with-debug-info', '--test-mode']
114
115    if target == 'valgrind':
116        flags += ['--with-valgrind']
117        test_prefix = ['valgrind', '--error-exitcode=9', '-v', '--leak-check=full', '--show-reachable=yes']
118        # valgrind is single threaded anyway
119        test_cmd += ['--test-threads=1']
120        # valgrind is slow
121        slow_tests = [
122            'cryptobox', 'dh_invalid', 'dh_kat', 'dh_keygen',
123            'dl_group_gen', 'dlies', 'dsa_param', 'ecc_basemul',
124            'ecdsa_verify_wycheproof', 'mce_keygen', 'passhash9',
125            'rsa_encrypt', 'rsa_pss', 'rsa_pss_raw', 'scrypt',
126            'srp6_kat', 'x509_path_bsi', 'xmss_keygen', 'xmss_sign',
127            'pbkdf', 'argon2', 'bcrypt', 'bcrypt_pbkdf', 'compression',
128            'ed25519_sign', 'elgamal_keygen', 'x509_path_rsa_pss']
129
130        disabled_tests += slow_tests
131
132    if target == 'fuzzers':
133        flags += ['--unsafe-fuzzer-mode']
134
135    if target in ['fuzzers', 'coverage']:
136        flags += ['--build-fuzzers=test']
137
138    if target in ['fuzzers', 'sanitizer']:
139        flags += ['--with-debug-asserts']
140
141        if target_cc in ['clang', 'gcc']:
142            flags += ['--enable-sanitizers=address,undefined']
143        else:
144            flags += ['--with-sanitizers']
145
146    if target in ['valgrind', 'sanitizer', 'fuzzers']:
147        flags += ['--disable-modules=locking_allocator']
148
149    if target == 'baremetal':
150        cc_bin = 'arm-none-eabi-c++'
151        flags += ['--cpu=arm32', '--disable-neon', '--without-stack-protector', '--ldflags=-specs=nosys.specs']
152        test_cmd = None
153
154    if is_cross_target:
155        if target_os == 'ios':
156            make_prefix = ['xcrun', '--sdk', 'iphoneos']
157            test_cmd = None
158            if target == 'cross-ios-arm64':
159                flags += ['--cpu=arm64', '--cc-abi-flags=-arch arm64 -stdlib=libc++']
160            else:
161                raise Exception("Unknown cross target '%s' for iOS" % (target))
162        elif target_os == 'android':
163
164            ndk = os.getenv('ANDROID_NDK')
165            if ndk is None:
166                raise Exception('Android CI build requires ANDROID_NDK env variable be set')
167
168            api_lvl = int(os.getenv('ANDROID_API_LEVEL', '0'))
169            if api_lvl == 0:
170                # If not set arbitrarily choose API 16 (Android 4.1) for ARMv7 and 28 (Android 9) for AArch64
171                api_lvl = 16 if target == 'cross-android-arm32' else 28
172
173            toolchain_dir = os.path.join(ndk, 'toolchains/llvm/prebuilt/linux-x86_64/bin')
174            test_cmd = None
175
176            if target == 'cross-android-arm32':
177                cc_bin = os.path.join(toolchain_dir, 'armv7a-linux-androideabi%d-clang++' % (api_lvl))
178                flags += ['--cpu=armv7',
179                          '--ar-command=%s' % (os.path.join(toolchain_dir, 'arm-linux-androideabi-ar'))]
180            elif target == 'cross-android-arm64':
181                cc_bin = os.path.join(toolchain_dir, 'aarch64-linux-android%d-clang++' % (api_lvl))
182                flags += ['--cpu=arm64',
183                          '--ar-command=%s' % (os.path.join(toolchain_dir, 'aarch64-linux-android-ar'))]
184
185            if api_lvl < 18:
186                flags += ['--without-os-features=getauxval']
187            if api_lvl >= 28:
188                flags += ['--with-os-features=getentropy']
189
190        elif target == 'cross-i386':
191            flags += ['--cpu=x86_32']
192
193        elif target == 'cross-win64':
194            # MinGW in 16.04 is lacking std::mutex for unknown reason
195            cc_bin = 'x86_64-w64-mingw32-g++'
196            flags += ['--cpu=x86_64', '--cc-abi-flags=-static',
197                      '--ar-command=x86_64-w64-mingw32-ar', '--without-os-feature=threads']
198            test_cmd = [os.path.join(root_dir, 'botan-test.exe')] + test_cmd[1:]
199            test_prefix = ['wine']
200        else:
201            if target == 'cross-arm32':
202                flags += ['--cpu=armv7']
203                cc_bin = 'arm-linux-gnueabihf-g++'
204                # Currently arm32 CI only runs on native AArch64
205                #test_prefix = ['qemu-arm', '-L', '/usr/arm-linux-gnueabihf/']
206            elif target == 'cross-arm64':
207                flags += ['--cpu=aarch64']
208                cc_bin = 'aarch64-linux-gnu-g++'
209                test_prefix = ['qemu-aarch64', '-L', '/usr/aarch64-linux-gnu/']
210            elif target == 'cross-ppc32':
211                flags += ['--cpu=ppc32']
212                cc_bin = 'powerpc-linux-gnu-g++'
213                test_prefix = ['qemu-ppc', '-L', '/usr/powerpc-linux-gnu/']
214            elif target == 'cross-ppc64':
215                flags += ['--cpu=ppc64', '--with-endian=little']
216                cc_bin = 'powerpc64le-linux-gnu-g++'
217                test_prefix = ['qemu-ppc64le', '-cpu', 'POWER8', '-L', '/usr/powerpc64le-linux-gnu/']
218            elif target == 'cross-mips64':
219                flags += ['--cpu=mips64', '--with-endian=big']
220                cc_bin = 'mips64-linux-gnuabi64-g++'
221                test_prefix = ['qemu-mips64', '-L', '/usr/mips64-linux-gnuabi64/']
222                test_cmd.remove('simd_32') # no SIMD on MIPS
223            else:
224                raise Exception("Unknown cross target '%s' for Linux" % (target))
225    else:
226        # Flags specific to native targets
227
228        if target_os in ['osx', 'linux']:
229            flags += ['--with-bzip2', '--with-sqlite', '--with-zlib']
230
231        if target_os in ['osx', 'ios']:
232            flags += ['--with-commoncrypto']
233
234        if target == 'coverage':
235            flags += ['--with-boost']
236
237        if target_os == 'windows' and target in ['shared', 'static']:
238            # ./configure.py needs extra hand-holding for boost on windows
239            boost_root = os.environ.get('BOOST_ROOT')
240            boost_libs = os.environ.get('BOOST_LIBRARYDIR')
241            boost_system = os.environ.get('BOOST_SYSTEM_LIBRARY')
242
243            if boost_root and boost_libs and boost_system:
244                flags += ['--with-boost',
245                          '--with-external-includedir', boost_root,
246                          '--with-external-libdir', boost_libs,
247                          '--boost-library-name', boost_system]
248
249        if target_os == 'linux':
250            flags += ['--with-lzma']
251
252        if target_os == 'linux':
253            if target not in ['sanitizer', 'valgrind', 'minimized']:
254                # Avoid OpenSSL when using dynamic checkers, or on OS X where it sporadically
255                # is not installed on the CI image
256                flags += ['--with-openssl']
257
258        if target in ['coverage']:
259            flags += ['--with-tpm']
260            test_cmd += ['--run-online-tests']
261            if pkcs11_lib and os.access(pkcs11_lib, os.R_OK):
262                test_cmd += ['--pkcs11-lib=%s' % (pkcs11_lib)]
263
264    if target in ['coverage', 'sanitizer']:
265        test_cmd += ['--run-long-tests']
266
267    flags += ['--cc-bin=%s' % (cc_bin)]
268
269    if test_cmd is None:
270        run_test_command = None
271    else:
272        if use_gdb:
273            disabled_tests.append("os_utils")
274
275        # render 'disabled_tests' array into test_cmd
276        if disabled_tests:
277            test_cmd += ['--skip-tests=%s' % (','.join(disabled_tests))]
278
279        if use_gdb:
280            (cmd, args) = test_cmd[0], test_cmd[1:]
281            run_test_command = test_prefix + ['gdb', cmd,
282                                              '-ex', 'run %s' % (' '.join(args)),
283                                              '-ex', 'bt',
284                                              '-ex', 'quit']
285        else:
286            run_test_command = test_prefix + test_cmd
287
288    return flags, run_test_command, make_prefix
289
290def run_cmd(cmd, root_dir):
291    """
292    Execute a command, die if it failed
293    """
294    print("Running '%s' ..." % (' '.join(cmd)))
295    sys.stdout.flush()
296
297    start = time.time()
298
299    cmd = [os.path.expandvars(elem) for elem in cmd]
300    sub_env = os.environ.copy()
301    sub_env['LD_LIBRARY_PATH'] = os.path.abspath(root_dir)
302    sub_env['DYLD_LIBRARY_PATH'] = os.path.abspath(root_dir)
303    sub_env['PYTHONPATH'] = os.path.abspath(os.path.join(root_dir, 'src/python'))
304    cwd = None
305
306    redirect_stdout = None
307    if len(cmd) >= 3 and cmd[-2] == '>':
308        redirect_stdout = open(cmd[-1], 'w')
309        cmd = cmd[:-2]
310    if len(cmd) > 1 and cmd[0].startswith('indir:'):
311        cwd = cmd[0][6:]
312        cmd = cmd[1:]
313    while len(cmd) > 1 and cmd[0].startswith('env:') and cmd[0].find('=') > 0:
314        env_key, env_val = cmd[0][4:].split('=')
315        sub_env[env_key] = env_val
316        cmd = cmd[1:]
317
318    proc = subprocess.Popen(cmd, cwd=cwd, close_fds=True, env=sub_env, stdout=redirect_stdout)
319    proc.communicate()
320
321    time_taken = int(time.time() - start)
322
323    if time_taken > 10:
324        print("Ran for %d seconds" % (time_taken))
325
326    if proc.returncode != 0:
327        print("Command '%s' failed with error code %d" % (' '.join(cmd), proc.returncode))
328
329        if cmd[0] not in ['lcov']:
330            sys.exit(proc.returncode)
331
332def default_os():
333    platform_os = platform.system().lower()
334    if platform_os == 'darwin':
335        return 'osx'
336    return platform_os
337
338def parse_args(args):
339    """
340    Parse arguments
341    """
342    parser = optparse.OptionParser()
343
344    parser.add_option('--os', default=default_os(),
345                      help='Set the target os (default %default)')
346    parser.add_option('--cc', default='gcc',
347                      help='Set the target compiler type (default %default)')
348    parser.add_option('--cc-bin', default=None,
349                      help='Set path to compiler')
350    parser.add_option('--root-dir', metavar='D', default='.',
351                      help='Set directory to execute from (default %default)')
352
353    parser.add_option('--make-tool', metavar='TOOL', default='make',
354                      help='Specify tool to run to build source (default %default)')
355
356    parser.add_option('--extra-cxxflags', metavar='FLAGS', default=[], action='append',
357                      help='Specify extra build flags')
358
359    parser.add_option('--cpu', default=None,
360                      help='Specify a target CPU platform')
361
362    parser.add_option('--with-debug', action='store_true', default=False,
363                      help='Include debug information')
364    parser.add_option('--amalgamation', action='store_true', default=False,
365                      help='Build via amalgamation')
366    parser.add_option('--disable-shared', action='store_true', default=False,
367                      help='Disable building shared libraries')
368    parser.add_option('--disabled-tests', metavar='DISABLED_TESTS', default=[], action='append',
369                      help='Comma separated list of tests that should not be run')
370
371    parser.add_option('--branch', metavar='B', default=None,
372                      help='Specify branch being built')
373
374    parser.add_option('--dry-run', action='store_true', default=False,
375                      help='Just show commands to be executed')
376    parser.add_option('--build-jobs', metavar='J', default=get_concurrency(),
377                      help='Set number of jobs to run in parallel (default %default)')
378
379    parser.add_option('--compiler-cache', default=None, metavar='CC',
380                      help='Set a compiler cache to use (ccache, sccache)')
381
382    parser.add_option('--pkcs11-lib', default=os.getenv('PKCS11_LIB'), metavar='LIB',
383                      help='Set PKCS11 lib to use for testing')
384
385    parser.add_option('--with-python3', dest='use_python3', action='store_true', default=None,
386                      help='Enable using python3')
387    parser.add_option('--without-python3', dest='use_python3', action='store_false',
388                      help='Disable using python3')
389
390    parser.add_option('--with-pylint3', dest='use_pylint3', action='store_true', default=True,
391                      help='Enable using python3 pylint')
392    parser.add_option('--without-pylint3', dest='use_pylint3', action='store_false',
393                      help='Disable using python3 pylint')
394
395    parser.add_option('--disable-werror', action='store_true', default=False,
396                      help='Allow warnings to compile')
397
398    parser.add_option('--run-under-gdb', dest='use_gdb', action='store_true', default=False,
399                      help='Run test suite under gdb and capture backtrace')
400
401    return parser.parse_args(args)
402
403def have_prog(prog):
404    """
405    Check if some named program exists in the path
406    """
407    for path in os.environ['PATH'].split(os.pathsep):
408        exe_file = os.path.join(path, prog)
409        if os.path.exists(exe_file) and os.access(exe_file, os.X_OK):
410            return True
411    return False
412
413def main(args=None):
414    # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements,too-many-locals
415    """
416    Parse options, do the things
417    """
418
419    if os.getenv('COVERITY_SCAN_BRANCH') == '1':
420        print('Skipping build COVERITY_SCAN_BRANCH set in environment')
421        return 0
422
423    if args is None:
424        args = sys.argv
425    print("Invoked as '%s'" % (' '.join(args)))
426    (options, args) = parse_args(args)
427
428    if len(args) != 2:
429        print('Usage: %s [options] target' % (args[0]))
430        return 1
431
432    target = args[1]
433
434    if options.use_python3 is None:
435        use_python3 = have_prog('python3')
436    else:
437        use_python3 = options.use_python3
438
439    py_interp = 'python'
440    if use_python3:
441        py_interp = 'python3'
442
443    if options.cc_bin is None:
444        if options.cc == 'gcc':
445            options.cc_bin = 'g++'
446        elif options.cc == 'clang':
447            options.cc_bin = 'clang++'
448        elif options.cc == 'msvc':
449            options.cc_bin = 'cl'
450        else:
451            print('Error unknown compiler %s' % (options.cc))
452            return 1
453
454    if options.compiler_cache is None and options.cc != 'msvc':
455        # Autodetect ccache
456        if have_prog('ccache'):
457            options.compiler_cache = 'ccache'
458
459    if options.compiler_cache not in [None, 'ccache', 'sccache']:
460        raise Exception("Don't know about %s as a compiler cache" % (options.compiler_cache))
461
462    root_dir = options.root_dir
463
464    if not os.access(root_dir, os.R_OK):
465        raise Exception('Bad root dir setting, dir %s not readable' % (root_dir))
466
467    cmds = []
468
469    if target == 'lint':
470
471        pylint_rc = '--rcfile=%s' % (os.path.join(root_dir, 'src/configs/pylint.rc'))
472        pylint_flags = [pylint_rc, '--reports=no']
473
474        # Some disabled rules specific to Python3
475        # useless-object-inheritance: complains about code still useful in Python2
476        py3_flags = '--disable=useless-object-inheritance'
477
478        py_scripts = [
479            'configure.py',
480            'src/python/botan2.py',
481            'src/scripts/ci_build.py',
482            'src/scripts/install.py',
483            'src/scripts/ci_check_install.py',
484            'src/scripts/dist.py',
485            'src/scripts/cleanup.py',
486            'src/scripts/check.py',
487            'src/scripts/build_docs.py',
488            'src/scripts/website.py',
489            'src/scripts/bench.py',
490            'src/scripts/test_python.py',
491            'src/scripts/test_fuzzers.py',
492            'src/scripts/test_cli.py',
493            'src/scripts/python_unittests.py',
494            'src/scripts/python_unittests_unix.py']
495
496        full_paths = [os.path.join(root_dir, s) for s in py_scripts]
497
498        if use_python3 and options.use_pylint3:
499            cmds.append(['python3', '-m', 'pylint'] + pylint_flags + [py3_flags] + full_paths)
500
501    else:
502        config_flags, run_test_command, make_prefix = determine_flags(
503            target, options.os, options.cpu, options.cc,
504            options.cc_bin, options.compiler_cache, root_dir,
505            options.pkcs11_lib, options.use_gdb, options.disable_werror,
506            options.extra_cxxflags, options.disabled_tests)
507
508        cmds.append([py_interp, os.path.join(root_dir, 'configure.py')] + config_flags)
509
510        make_cmd = [options.make_tool]
511        if root_dir != '.':
512            make_cmd += ['-C', root_dir]
513        if options.build_jobs > 1 and options.make_tool != 'nmake':
514            make_cmd += ['-j%d' % (options.build_jobs)]
515        make_cmd += ['-k']
516
517        if target == 'docs':
518            cmds.append(make_cmd + ['docs'])
519        else:
520            if options.compiler_cache is not None:
521                cmds.append([options.compiler_cache, '--show-stats'])
522
523            make_targets = ['libs', 'tests', 'cli']
524
525            if target in ['coverage', 'fuzzers']:
526                make_targets += ['fuzzer_corpus_zip', 'fuzzers']
527
528            if target in ['coverage']:
529                make_targets += ['bogo_shim']
530
531            cmds.append(make_prefix + make_cmd + make_targets)
532
533            if options.compiler_cache is not None:
534                cmds.append([options.compiler_cache, '--show-stats'])
535
536        if run_test_command is not None:
537            cmds.append(run_test_command)
538
539        if target == 'coverage':
540            runner_dir = os.path.abspath(os.path.join(root_dir, 'boringssl', 'ssl', 'test', 'runner'))
541
542            cmds.append(['indir:%s' % (runner_dir),
543                         'go', 'test', '-pipe',
544                         '-num-workers', str(4*get_concurrency()),
545                         '-shim-path', os.path.abspath(os.path.join(root_dir, 'botan_bogo_shim')),
546                         '-shim-config', os.path.abspath(os.path.join(root_dir, 'src', 'bogo_shim', 'config.json'))])
547
548        if target in ['coverage', 'fuzzers']:
549            cmds.append([py_interp, os.path.join(root_dir, 'src/scripts/test_fuzzers.py'),
550                         os.path.join(root_dir, 'fuzzer_corpus'),
551                         os.path.join(root_dir, 'build/fuzzer')])
552
553        if target in ['shared', 'coverage'] and options.os != 'windows':
554            botan_exe = os.path.join(root_dir, 'botan-cli.exe' if options.os == 'windows' else 'botan')
555
556            args = ['--threads=%d' % (options.build_jobs)]
557            test_scripts = ['test_cli.py', 'test_cli_crypt.py']
558            for script in test_scripts:
559                cmds.append([py_interp, os.path.join(root_dir, 'src/scripts', script)] +
560                            args + [botan_exe])
561
562        python_tests = os.path.join(root_dir, 'src/scripts/test_python.py')
563
564        if target in ['shared', 'coverage']:
565
566            if options.os == 'windows':
567                if options.cpu == 'x86':
568                    # Python on AppVeyor is a 32-bit binary so only test for 32-bit
569                    cmds.append([py_interp, '-b', python_tests])
570            else:
571                if use_python3:
572                    cmds.append(['python3', '-b', python_tests])
573
574        if target in ['shared', 'static', 'bsi', 'nist']:
575            cmds.append(make_cmd + ['install'])
576            build_config = os.path.join(root_dir, 'build', 'build_config.json')
577            cmds.append([py_interp, os.path.join(root_dir, 'src/scripts/ci_check_install.py'), build_config])
578
579        if target in ['coverage']:
580            if not have_prog('lcov'):
581                print('Error: lcov not found in PATH (%s)' % (os.getenv('PATH')))
582                return 1
583
584            if not have_prog('gcov'):
585                print('Error: gcov not found in PATH (%s)' % (os.getenv('PATH')))
586                return 1
587
588            cov_file = 'coverage.info'
589            raw_cov_file = 'coverage.info.raw'
590
591            cmds.append(['lcov', '--capture', '--directory', options.root_dir,
592                         '--output-file', raw_cov_file])
593            cmds.append(['lcov', '--remove', raw_cov_file, '/usr/*', '--output-file', cov_file])
594            cmds.append(['lcov', '--list', cov_file])
595
596            if have_prog('coverage'):
597                cmds.append(['coverage', 'run', '--branch',
598                             '--rcfile', os.path.join(root_dir, 'src/configs/coverage.rc'),
599                             python_tests])
600
601            if have_prog('codecov'):
602                # If codecov exists assume we are in CI and report to codecov.io
603                cmds.append(['codecov', '>', 'codecov_stdout.log'])
604            else:
605                # Otherwise generate a local HTML report
606                cmds.append(['genhtml', cov_file, '--output-directory', 'lcov-out'])
607
608        cmds.append(make_cmd + ['clean'])
609        cmds.append(make_cmd + ['distclean'])
610
611    for cmd in cmds:
612        if options.dry_run:
613            print('$ ' + ' '.join(cmd))
614        else:
615            run_cmd(cmd, root_dir)
616
617    return 0
618
619if __name__ == '__main__':
620    sys.exit(main())
621