~ubuntu-branches/debian/sid/botan/sid

« back to all changes in this revision

Viewing changes to src/scripts/ci_build.py

  • Committer: Package Import Robot
  • Author(s): Laszlo Boszormenyi (GCS)
  • Date: 2018-03-01 22:23:25 UTC
  • mfrom: (1.2.2)
  • Revision ID: package-import@ubuntu.com-20180301222325-7p7vc45gu3hta34d
Tags: 2.4.0-2
* Don't remove .doctrees from the manual if it doesn't exist.
* Don't specify parallel to debhelper.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
"""
 
4
CI build script
 
5
(C) 2017 Jack Lloyd
 
6
Botan is released under the Simplified BSD License (see license.txt)
 
7
"""
 
8
 
 
9
import os
 
10
import platform
 
11
import subprocess
 
12
import sys
 
13
import time
 
14
import tempfile
 
15
import optparse # pylint: disable=deprecated-module
 
16
 
 
17
def get_concurrency():
 
18
    """
 
19
    Get default concurrency level of build
 
20
    """
 
21
    def_concurrency = 2
 
22
 
 
23
    try:
 
24
        import multiprocessing
 
25
        return max(def_concurrency, multiprocessing.cpu_count())
 
26
    except ImportError:
 
27
        return def_concurrency
 
28
 
 
29
def determine_flags(target, target_os, target_cpu, target_cc, cc_bin, ccache, root_dir, pkcs11_lib):
 
30
    # pylint: disable=too-many-branches,too-many-statements,too-many-arguments,too-many-locals
 
31
 
 
32
    """
 
33
    Return the configure.py flags as well as make/test running prefixes
 
34
    """
 
35
    is_cross_target = target.startswith('cross-')
 
36
 
 
37
    if target_os not in ['linux', 'osx', 'windows']:
 
38
        print('Error unknown OS %s' % (target_os))
 
39
        return (None, None, None)
 
40
 
 
41
    if is_cross_target:
 
42
        if target_os == 'osx':
 
43
            target_os = 'ios'
 
44
        elif target == 'cross-win32':
 
45
            target_os = 'mingw'
 
46
 
 
47
    make_prefix = []
 
48
    test_prefix = []
 
49
    test_cmd = [os.path.join(root_dir, 'botan-test')]
 
50
 
 
51
    fast_tests = ['block', 'aead', 'hash', 'stream', 'mac', 'modes',
 
52
                  'hmac_drbg', 'hmac_drbg_unit',
 
53
                  'tls', 'ffi',
 
54
                  'rsa_sign', 'rsa_verify', 'dh_kat', 'ecdsa_sign', 'curve25519_scalar',
 
55
                  'simd_32', 'os_utils', 'util', 'util_dates']
 
56
 
 
57
    install_prefix = os.path.join(tempfile.gettempdir(), 'botan-install')
 
58
    flags = ['--prefix=%s' % (install_prefix),
 
59
             '--cc=%s' % (target_cc),
 
60
             '--os=%s' % (target_os)]
 
61
 
 
62
    if target_cpu != None:
 
63
        flags += ['--cpu=%s' % (target_cpu)]
 
64
 
 
65
    if target in ['shared', 'mini-shared']:
 
66
        flags += ['--disable-static']
 
67
 
 
68
    if target in ['static', 'mini-static', 'fuzzers'] or target_os in ['ios', 'mingw']:
 
69
        flags += ['--disable-shared']
 
70
 
 
71
    if target in ['mini-static', 'mini-shared']:
 
72
        flags += ['--minimized-build', '--enable-modules=system_rng,sha2_32,sha2_64,aes']
 
73
 
 
74
    if target == 'shared' and target_os != 'osx':
 
75
        # Enabling amalgamation build for shared is somewhat arbitrary, but we want to test it
 
76
        # somewhere. In addition the majority of the Windows builds are shared, and MSVC is
 
77
        # much faster compiling via the amalgamation than individual files.
 
78
        flags += ['--amalgamation']
 
79
 
 
80
    if target in ['bsi', 'nist']:
 
81
        # Arbitrarily test disable static on module policy builds
 
82
        flags += ['--module-policy=%s' % (target), '--disable-static']
 
83
 
 
84
    if target == 'docs':
 
85
        flags += ['--with-doxygen', '--with-sphinx', '--with-rst2man']
 
86
        test_cmd = None
 
87
 
 
88
    if target == 'coverage':
 
89
        flags += ['--with-coverage-info']
 
90
    if target == 'valgrind':
 
91
        flags += ['--with-valgrind']
 
92
        test_prefix = ['valgrind', '--error-exitcode=9', '-v', '--leak-check=full', '--show-reachable=yes']
 
93
        test_cmd += fast_tests
 
94
    if target == 'fuzzers':
 
95
        flags += ['--unsafe-fuzzer-mode']
 
96
 
 
97
    if target in ['fuzzers', 'coverage', 'valgrind']:
 
98
        flags += ['--with-debug-info']
 
99
    if target in ['fuzzers', 'coverage']:
 
100
        flags += ['--build-fuzzers=test']
 
101
    if target in ['fuzzers', 'sanitizer']:
 
102
 
 
103
        # On VC iterator debugging comes from generic debug mode
 
104
        if target_cc == 'msvc':
 
105
            flags += ['--with-debug-info']
 
106
        else:
 
107
            flags += ['--with-sanitizers']
 
108
    if target in ['valgrind', 'sanitizer', 'fuzzers']:
 
109
        flags += ['--disable-modules=locking_allocator']
 
110
 
 
111
    if target == 'parallel':
 
112
        flags += ['--with-openmp']
 
113
 
 
114
    if target == 'sonar':
 
115
        if target_os != 'linux' or target_cc != 'clang':
 
116
            raise Exception('Only Linux/clang supported in Sonar target currently')
 
117
 
 
118
        flags += ['--cc-abi-flags=-fprofile-instr-generate -fcoverage-mapping',
 
119
                  '--disable-shared']
 
120
 
 
121
        make_prefix = [os.path.join(root_dir, 'build-wrapper-linux-x86/build-wrapper-linux-x86-64'),
 
122
                       '--out-dir', 'bw-outputs']
 
123
 
 
124
    if is_cross_target:
 
125
        if target_os == 'ios':
 
126
            make_prefix = ['xcrun', '--sdk', 'iphoneos']
 
127
            test_cmd = None
 
128
            if target == 'cross-arm32':
 
129
                flags += ['--cpu=armv7', '--cc-abi-flags=-arch armv7 -arch armv7s -stdlib=libc++']
 
130
            elif target == 'cross-arm64':
 
131
                flags += ['--cpu=armv8-a', '--cc-abi-flags=-arch arm64 -stdlib=libc++']
 
132
            else:
 
133
                raise Exception("Unknown cross target '%s' for iOS" % (target))
 
134
        elif target == 'cross-win32':
 
135
            cc_bin = 'i686-w64-mingw32-g++'
 
136
            flags += ['--cpu=x86_32', '--cc-abi-flags=-static', '--ar-command=i686-w64-mingw32-ar']
 
137
            test_cmd = [os.path.join(root_dir, 'botan-test.exe')]
 
138
            # No runtime prefix required for Wine
 
139
        else:
 
140
            # Build everything but restrict what is run
 
141
            test_cmd += fast_tests
 
142
 
 
143
            if target == 'cross-arm32':
 
144
                flags += ['--cpu=armv7']
 
145
                cc_bin = 'arm-linux-gnueabihf-g++'
 
146
                test_prefix = ['qemu-arm', '-L', '/usr/arm-linux-gnueabihf/']
 
147
            elif target == 'cross-arm64':
 
148
                flags += ['--cpu=armv8-a']
 
149
                cc_bin = 'aarch64-linux-gnu-g++'
 
150
                test_prefix = ['qemu-aarch64', '-L', '/usr/aarch64-linux-gnu/']
 
151
            elif target == 'cross-ppc32':
 
152
                flags += ['--cpu=ppc32']
 
153
                cc_bin = 'powerpc-linux-gnu-g++'
 
154
                test_prefix = ['qemu-ppc', '-L', '/usr/powerpc-linux-gnu/']
 
155
            elif target == 'cross-ppc64':
 
156
                flags += ['--cpu=ppc64', '--with-endian=little']
 
157
                cc_bin = 'powerpc64le-linux-gnu-g++'
 
158
                test_prefix = ['qemu-ppc64le', '-L', '/usr/powerpc64le-linux-gnu/']
 
159
            else:
 
160
                raise Exception("Unknown cross target '%s' for Linux" % (target))
 
161
    else:
 
162
        # Flags specific to native targets
 
163
 
 
164
        if target_os in ['osx', 'linux']:
 
165
            flags += ['--with-bzip2', '--with-sqlite', '--with-zlib']
 
166
 
 
167
        if target_os == 'osx':
 
168
            # Test Boost on OS X
 
169
            flags += ['--with-boost']
 
170
        elif target_os == 'linux':
 
171
            flags += ['--with-lzma']
 
172
 
 
173
        if target_os == 'linux':
 
174
            if target not in ['sanitizer', 'valgrind', 'mini-shared', 'mini-static']:
 
175
                # Avoid OpenSSL when using dynamic checkers, or on OS X where it sporadically
 
176
                # is not installed on the CI image
 
177
                flags += ['--with-openssl']
 
178
 
 
179
        if target in ['sonar', 'coverage']:
 
180
            flags += ['--with-tpm']
 
181
            test_cmd += ['--run-long-tests', '--run-online-tests']
 
182
            if pkcs11_lib and os.access(pkcs11_lib, os.R_OK):
 
183
                test_cmd += ['--pkcs11-lib=%s' % (pkcs11_lib)]
 
184
 
 
185
    if ccache is None:
 
186
        flags += ['--cc-bin=%s' % (cc_bin)]
 
187
    elif ccache == 'clcache':
 
188
        flags += ['--cc-bin=%s' % (ccache)]
 
189
    else:
 
190
        flags += ['--cc-bin=%s %s' % (ccache, cc_bin)]
 
191
 
 
192
    if test_cmd is None:
 
193
        run_test_command = None
 
194
    else:
 
195
        run_test_command = test_prefix + test_cmd
 
196
 
 
197
    return flags, run_test_command, make_prefix
 
198
 
 
199
def run_cmd(cmd, root_dir):
 
200
    """
 
201
    Execute a command, die if it failed
 
202
    """
 
203
    print("Running '%s' ..." % (' '.join(cmd)))
 
204
    sys.stdout.flush()
 
205
 
 
206
    start = time.time()
 
207
 
 
208
    cmd = [os.path.expandvars(elem) for elem in cmd]
 
209
    sub_env = os.environ.copy()
 
210
    sub_env['LD_LIBRARY_PATH'] = root_dir
 
211
 
 
212
    redirect_stdout = None
 
213
    if len(cmd) > 3 and cmd[-2] == '>':
 
214
        redirect_stdout = open(cmd[-1], 'w')
 
215
        cmd = cmd[:-2]
 
216
    proc = subprocess.Popen(cmd, close_fds=True, env=sub_env, stdout=redirect_stdout)
 
217
    proc.communicate()
 
218
 
 
219
    time_taken = int(time.time() - start)
 
220
 
 
221
    if time_taken > 10:
 
222
        print("Ran for %d seconds" % (time_taken))
 
223
 
 
224
    if proc.returncode != 0:
 
225
        print("Command failed with error code %d" % (proc.returncode))
 
226
        sys.exit(proc.returncode)
 
227
 
 
228
def parse_args(args):
 
229
    """
 
230
    Parse arguments
 
231
    """
 
232
    parser = optparse.OptionParser()
 
233
 
 
234
    parser.add_option('--os', default=platform.system().lower(),
 
235
                      help='Set the target os (default %default)')
 
236
    parser.add_option('--cc', default='gcc',
 
237
                      help='Set the target compiler type (default %default)')
 
238
    parser.add_option('--cc-bin', default=None,
 
239
                      help='Set path to compiler')
 
240
    parser.add_option('--root-dir', metavar='D', default='.',
 
241
                      help='Set directory to execute from (default %default)')
 
242
 
 
243
    parser.add_option('--make-tool', metavar='TOOL', default='make',
 
244
                      help='Specify tool to run to build source (default %default)')
 
245
 
 
246
    parser.add_option('--cpu', default=None,
 
247
                      help='Specify a target CPU platform')
 
248
 
 
249
    parser.add_option('--with-debug', action='store_true', default=False,
 
250
                      help='Include debug information')
 
251
    parser.add_option('--amalgamation', action='store_true', default=False,
 
252
                      help='Build via amalgamation')
 
253
    parser.add_option('--disable-shared', action='store_true', default=False,
 
254
                      help='Disable building shared libraries')
 
255
 
 
256
    parser.add_option('--branch', metavar='B', default=None,
 
257
                      help='Specify branch being built')
 
258
 
 
259
    parser.add_option('--add-travis-folds', action='store_true', default=False,
 
260
                      help='Add fold markers for Travis UI')
 
261
 
 
262
    parser.add_option('--dry-run', action='store_true', default=False,
 
263
                      help='Just show commands to be executed')
 
264
    parser.add_option('--build-jobs', metavar='J', default=get_concurrency(),
 
265
                      help='Set number of jobs to run in parallel (default %default)')
 
266
 
 
267
    parser.add_option('--compiler-cache', default=None,
 
268
                      help='Set a compiler cache to use (ccache, clcache)')
 
269
 
 
270
    parser.add_option('--pkcs11-lib', default=None,
 
271
                      help='Set PKCS11 lib to use for testing')
 
272
 
 
273
    parser.add_option('--with-python3', dest='use_python3', action='store_true', default=None,
 
274
                      help='Enable using python3')
 
275
    parser.add_option('--without-python3', dest='use_python3', action='store_false',
 
276
                      help='Disable using python3')
 
277
 
 
278
    return parser.parse_args(args)
 
279
 
 
280
def have_prog(prog):
 
281
    """
 
282
    Check if some named program exists in the path
 
283
    """
 
284
    for path in os.environ['PATH'].split(os.pathsep):
 
285
        exe_file = os.path.join(path, prog)
 
286
        if os.path.exists(exe_file) and os.access(exe_file, os.X_OK):
 
287
            return True
 
288
    return False
 
289
 
 
290
def main(args=None):
 
291
    # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
 
292
    """
 
293
    Parse options, do the things
 
294
    """
 
295
 
 
296
    if os.getenv('COVERITY_SCAN_BRANCH') == '1':
 
297
        print('Skipping build COVERITY_SCAN_BRANCH set in environment')
 
298
        return 0
 
299
 
 
300
    (options, args) = parse_args(args or sys.argv)
 
301
 
 
302
    if len(args) != 2:
 
303
        print('Usage: %s [options] target' % (args[0]))
 
304
        return 1
 
305
 
 
306
    target = args[1]
 
307
 
 
308
    py_interp = 'python'
 
309
 
 
310
    use_python2 = have_prog('python2')
 
311
 
 
312
    if options.use_python3 is None:
 
313
        use_python3 = have_prog('python3')
 
314
    else:
 
315
        use_python3 = options.use_python3
 
316
        if use_python3:
 
317
            py_interp = 'python3'
 
318
 
 
319
    if options.cc_bin is None:
 
320
        if options.cc == 'gcc':
 
321
            options.cc_bin = 'g++'
 
322
        elif options.cc == 'clang':
 
323
            options.cc_bin = 'clang++'
 
324
        elif options.cc == 'msvc':
 
325
            options.cc_bin = 'cl'
 
326
        else:
 
327
            print('Error unknown compiler %s' % (options.cc))
 
328
            return 1
 
329
 
 
330
    if options.compiler_cache is None and options.cc != 'msvc':
 
331
        # Autodetect ccache, unless using clang profiling - ccache seems to misbehave there
 
332
        if have_prog('ccache') and target not in ['sonar']:
 
333
            options.compiler_cache = 'ccache'
 
334
 
 
335
    if options.compiler_cache == 'clcache' and target in ['sanitizer']:
 
336
        # clcache doesn't support /Zi so using it just adds overhead with
 
337
        # no benefit
 
338
        options.compiler_cache = None
 
339
 
 
340
    if target == 'sonar' and os.getenv('SONAR_TOKEN') is None:
 
341
        print('Skipping Sonar scan due to missing SONAR_TOKEN env variable')
 
342
        return 0
 
343
 
 
344
    root_dir = options.root_dir
 
345
 
 
346
    if os.access(root_dir, os.R_OK) != True:
 
347
        raise Exception('Bad root dir setting, dir %s not readable' % (root_dir))
 
348
 
 
349
    cmds = []
 
350
 
 
351
    if target == 'lint':
 
352
 
 
353
        if not use_python2 and not use_python3:
 
354
            raise Exception('No python interpreters found cannot lint')
 
355
 
 
356
        pylint_rc = '--rcfile=%s' % (os.path.join(root_dir, 'src/configs/pylint.rc'))
 
357
        pylint_flags = [pylint_rc, '--reports=no', '--score=no']
 
358
 
 
359
        # Some disabled rules specific to Python2
 
360
        # superfluous-parens: needed for Python3 compatible print statements
 
361
        # too-many-locals: variable counting differs from pylint3
 
362
        py2_flags = '--disable=superfluous-parens,too-many-locals'
 
363
 
 
364
        py_scripts = [
 
365
            'configure.py',
 
366
            'src/python/botan2.py',
 
367
            'src/scripts/ci_build.py',
 
368
            'src/scripts/install.py',
 
369
            'src/scripts/dist.py',
 
370
            'src/scripts/cleanup.py',
 
371
            'src/scripts/build_docs.py',
 
372
            'src/scripts/website.py',
 
373
            'src/scripts/bench.py',
 
374
            'src/scripts/python_unittests.py',
 
375
            'src/scripts/python_unittests_unix.py']
 
376
 
 
377
        for target in py_scripts:
 
378
            target_path = os.path.join(root_dir, target)
 
379
 
 
380
            if use_python2:
 
381
                cmds.append(['python2', '-m', 'pylint'] + pylint_flags + [py2_flags, target_path])
 
382
 
 
383
            if use_python3:
 
384
                cmds.append(['python3', '-m', 'pylint'] + pylint_flags + [target_path])
 
385
 
 
386
    else:
 
387
        config_flags, run_test_command, make_prefix = determine_flags(
 
388
            target, options.os, options.cpu, options.cc,
 
389
            options.cc_bin, options.compiler_cache, root_dir,
 
390
            options.pkcs11_lib)
 
391
 
 
392
        cmds.append([py_interp, os.path.join(root_dir, 'configure.py')] + config_flags)
 
393
 
 
394
        make_cmd = [options.make_tool]
 
395
        if root_dir != '.':
 
396
            make_cmd += ['-C', root_dir]
 
397
        if options.build_jobs > 1:
 
398
            make_cmd += ['-j%d' % (options.build_jobs)]
 
399
        make_cmd += ['-k']
 
400
 
 
401
        if target == 'docs':
 
402
            cmds.append(make_cmd + ['docs'])
 
403
        else:
 
404
            if options.compiler_cache == 'ccache':
 
405
                cmds.append(['ccache', '--show-stats'])
 
406
            elif options.compiler_cache == 'clcache':
 
407
                cmds.append(['clcache', '-s'])
 
408
 
 
409
            make_targets = ['libs', 'cli', 'tests']
 
410
            if target in ['coverage', 'fuzzers']:
 
411
                make_targets += ['fuzzers', 'fuzzer_corpus_zip']
 
412
 
 
413
            cmds.append(make_prefix + make_cmd + make_targets)
 
414
 
 
415
            if options.compiler_cache == 'ccache':
 
416
                cmds.append(['ccache', '--show-stats'])
 
417
            elif options.compiler_cache == 'clcache':
 
418
                cmds.append(['clcache', '-s'])
 
419
 
 
420
        if run_test_command != None:
 
421
            cmds.append(run_test_command)
 
422
 
 
423
        if target in ['coverage', 'fuzzers']:
 
424
            cmds.append([py_interp, os.path.join(root_dir, 'src/scripts/test_fuzzers.py'),
 
425
                         os.path.join(root_dir, 'fuzzer_corpus'),
 
426
                         os.path.join(root_dir, 'build/fuzzer')])
 
427
 
 
428
        if target in ['static', 'shared'] and options.os != 'windows':
 
429
            botan_exe = os.path.join(root_dir, 'botan-cli.exe' if options.os == 'windows' else 'botan')
 
430
            cmds.append([py_interp,
 
431
                         os.path.join(root_dir, 'src/scripts/cli_tests.py'),
 
432
                         botan_exe])
 
433
 
 
434
        botan_py = os.path.join(root_dir, 'src/python/botan2.py')
 
435
 
 
436
        if target in ['shared', 'coverage']:
 
437
 
 
438
            if use_python2:
 
439
                cmds.append(['python2', botan_py])
 
440
            if use_python3:
 
441
                cmds.append(['python3', botan_py])
 
442
 
 
443
        if target in ['shared', 'static', 'bsi', 'nist']:
 
444
            cmds.append(make_cmd + ['install'])
 
445
 
 
446
        if target in ['sonar']:
 
447
 
 
448
            cmds.append(['llvm-profdata', 'merge', '-sparse', 'default.profraw', '-o', 'botan.profdata'])
 
449
            cmds.append(['llvm-cov', 'show', './botan-test',
 
450
                         '-instr-profile=botan.profdata',
 
451
                         '>', 'build/cov_report.txt'])
 
452
            sonar_config = os.path.join(root_dir, os.path.join(root_dir, 'src/build-data/sonar-project.properties'))
 
453
            cmds.append(['sonar-scanner',
 
454
                         '-Dproject.settings=%s' % (sonar_config),
 
455
                         '-Dsonar.login=$SONAR_TOKEN'])
 
456
 
 
457
        if target in ['coverage']:
 
458
 
 
459
            if not have_prog('lcov'):
 
460
                print('Error: lcov not found in PATH (%s)' % (os.getenv('PATH')))
 
461
                return 1
 
462
 
 
463
            if not have_prog('gcov'):
 
464
                print('Error: gcov not found in PATH (%s)' % (os.getenv('PATH')))
 
465
                return 1
 
466
 
 
467
            cov_file = 'coverage.info'
 
468
            raw_cov_file = 'coverage.info.raw'
 
469
 
 
470
            cmds.append(['lcov', '--capture', '--directory', options.root_dir,
 
471
                         '--output-file', raw_cov_file])
 
472
            cmds.append(['lcov', '--remove', raw_cov_file, '/usr/*', '--output-file', cov_file])
 
473
            cmds.append(['lcov', '--list', cov_file])
 
474
 
 
475
            if have_prog('coverage'):
 
476
                cmds.append(['coverage', 'run', '--branch',
 
477
                             '--rcfile', os.path.join(root_dir, 'src/configs/coverage.rc'),
 
478
                             botan_py])
 
479
 
 
480
            if have_prog('codecov'):
 
481
                # If codecov exists assume we are on Travis and report to codecov.io
 
482
                cmds.append(['codecov'])
 
483
            else:
 
484
                # Otherwise generate a local HTML report
 
485
                cmds.append(['genhtml', cov_file, '--output-directory', 'lcov-out'])
 
486
 
 
487
        cmds.append(make_cmd + ['clean'])
 
488
        cmds.append(make_cmd + ['distclean'])
 
489
 
 
490
    for cmd in cmds:
 
491
        if options.dry_run:
 
492
            print('$ ' + ' '.join(cmd))
 
493
        else:
 
494
            run_cmd(cmd, root_dir)
 
495
 
 
496
    return 0
 
497
 
 
498
if __name__ == '__main__':
 
499
    sys.exit(main())