1import glob
2import os
3import subprocess
4import sys
5import time
6
7from waflib import Configure, ConfigSet, Build, Context, Logs, Options, Utils
8from waflib.TaskGen import feature, before, after, after_method
9
10NONEMPTY = -10
11
12if sys.platform == 'win32':
13    lib_path_name = 'PATH'
14elif sys.platform == 'darwin':
15    lib_path_name = 'DYLD_LIBRARY_PATH'
16else:
17    lib_path_name = 'LD_LIBRARY_PATH'
18
19# Compute dependencies globally
20# import preproc
21# preproc.go_absolute = True
22
23
24@feature('c', 'cxx')
25@after('apply_incpaths')
26def include_config_h(self):
27    self.env.append_value('INCPATHS', self.bld.bldnode.abspath())
28
29
30def _set_system_headers(self, varname):
31    if 'AUTOWAF_SYSTEM_PKGS' in self.env and not self.env.MSVC_COMPILER:
32        for lib in self.uselib:
33            if lib in self.env.AUTOWAF_SYSTEM_PKGS:
34                for include in self.env['INCLUDES_' + lib]:
35                    self.env.append_unique(varname, ['-isystem%s' % include])
36
37
38@feature('c')
39@after_method('apply_incpaths')
40def set_system_headers_c(self):
41    _set_system_headers(self, 'CFLAGS')
42
43
44@feature('cxx')
45@after_method('apply_incpaths')
46def set_system_headers_cxx(self):
47    _set_system_headers(self, 'CXXFLAGS')
48
49
50class OptionsContext(Options.OptionsContext):
51    def __init__(self, **kwargs):
52        super(OptionsContext, self).__init__(**kwargs)
53        set_options(self)
54
55    def configuration_options(self):
56        return self.get_option_group('Configuration options')
57
58    def add_flags(self, group, flags):
59        """Tersely add flags (a dictionary of longname:desc) to a group"""
60        for name, desc in flags.items():
61            group.add_option('--' + name, action='store_true',
62                             dest=name.replace('-', '_'), help=desc)
63
64
65def set_options(opt):
66    "Add standard autowaf options"
67    opts = opt.get_option_group('Configuration options')
68
69    # Standard directory options
70    opts.add_option('--bindir', type='string',
71                    help="executable programs [default: PREFIX/bin]")
72    opts.add_option('--configdir', type='string',
73                    help="configuration data [default: PREFIX/etc]")
74    opts.add_option('--datadir', type='string',
75                    help="shared data [default: PREFIX/share]")
76    opts.add_option('--includedir', type='string',
77                    help="header files [default: PREFIX/include]")
78    opts.add_option('--libdir', type='string',
79                    help="libraries [default: PREFIX/lib]")
80    opts.add_option('--mandir', type='string',
81                    help="manual pages [default: DATADIR/man]")
82    opts.add_option('--docdir', type='string',
83                    help="HTML documentation [default: DATADIR/doc]")
84
85    # Build options
86    opts.add_option('-d', '--debug', action='store_true', default=False,
87                    dest='debug', help="build debuggable binaries")
88    opts.add_option('--pardebug', action='store_true', default=False,
89                    dest='pardebug',
90                    help="build debug libraries with D suffix")
91
92    opts.add_option('-s', '--strict', action='store_true', default=False,
93                    dest='strict',
94                    help="use strict compiler flags and show all warnings")
95    opts.add_option('-S', '--ultra-strict', action='store_true', default=False,
96                    dest='ultra_strict',
97                    help="use extremely strict compiler flags (likely noisy)")
98    opts.add_option('--docs', action='store_true', default=False, dest='docs',
99                    help="build documentation (requires doxygen)")
100    opts.add_option('-w', '--werror', action='store_true', dest='werror',
101                    help="Treat warnings as errors")
102
103    # Test options
104    if hasattr(Context.g_module, 'test'):
105        test_opts = opt.add_option_group('Test options', '')
106        opts.add_option('-T', '--test', action='store_true',
107                        dest='build_tests', help='build unit tests')
108        opts.add_option('--no-coverage', action='store_true',
109                        dest='no_coverage',
110                        help='do not instrument code for test coverage')
111        test_opts.add_option('--test-filter', type='string',
112                             dest='test_filter',
113                             help='regular expression for tests to run')
114
115    # Run options
116    run_opts = opt.add_option_group('Run options')
117    run_opts.add_option('--cmd', type='string', dest='cmd',
118                        help='command to run from build directory')
119    run_opts.add_option('--wrapper', type='string',
120                        dest='wrapper',
121                        help='command prefix for running executables')
122
123
124class ConfigureContext(Configure.ConfigurationContext):
125    """configures the project"""
126
127    def __init__(self, **kwargs):
128        self.line_just = 45
129        if hasattr(Context.g_module, 'line_just'):
130            self.line_just = Context.g_module.line_just
131
132        super(ConfigureContext, self).__init__(**kwargs)
133        self.run_env = ConfigSet.ConfigSet()
134
135    def pre_recurse(self, node):
136        if len(self.stack_path) == 1:
137            Logs.pprint('BOLD', 'Configuring %s' % node.parent.srcpath())
138        super(ConfigureContext, self).pre_recurse(node)
139
140    def store(self):
141        self.env.AUTOWAF_RUN_ENV = self.run_env.get_merged_dict()
142        super(ConfigureContext, self).store()
143
144    def check_pkg(self, *args, **kwargs):
145        return check_pkg(self, *args, **kwargs)
146
147    def check_function(self, *args, **kwargs):
148        return check_function(self, *args, **kwargs)
149
150    def build_path(self, path='.'):
151        """Return `path` within the build directory"""
152        return str(self.path.get_bld().make_node(path))
153
154
155def get_check_func(conf, lang):
156    if lang == 'c':
157        return conf.check_cc
158    elif lang == 'cxx':
159        return conf.check_cxx
160    else:
161        Logs.error("Unknown header language `%s'" % lang)
162
163
164def check_header(conf, lang, name, define='', mandatory=True):
165    "Check for a header"
166    check_func = get_check_func(conf, lang)
167    if define != '':
168        check_func(header_name=name,
169                   define_name=define,
170                   mandatory=mandatory)
171    else:
172        check_func(header_name=name, mandatory=mandatory)
173
174
175def check_function(conf, lang, name, **args):
176    "Check for a function"
177    header_names = Utils.to_list(args['header_name'])
178    includes = ''.join(['#include <%s>\n' % x for x in header_names])
179    return_type = args['return_type'] if 'return_type' in args else 'int'
180    arg_types = args['arg_types'] if 'arg_types' in args else ''
181
182    fragment = '''
183%s
184
185typedef %s (*Func)(%s);
186
187int main(void) {
188    static const Func ptr = %s;
189    (void)ptr;
190    return 0;
191}
192''' % (includes, return_type, arg_types, name)
193
194    check_func  = get_check_func(conf, lang)
195    args['msg'] = 'Checking for %s' % name
196    if lang + 'flags' not in args:
197        args[lang + 'flags'] = check_flags(conf, conf.env.CFLAGS)
198
199    check_func(fragment=fragment, **args)
200
201
202def nameify(name):
203    return (name.replace('/', '_').replace('++', 'PP')
204            .replace('-', '_').replace('.', '_'))
205
206
207def check_pkg(conf, spec, **kwargs):
208    "Check for a package iff it hasn't been checked for yet"
209
210    uselib_store = kwargs['uselib_store']
211    is_local = (uselib_store.lower() in conf.env['AUTOWAF_LOCAL_LIBS'] or
212                uselib_store.lower() in conf.env['AUTOWAF_LOCAL_HEADERS'])
213
214    if is_local:
215        return
216
217    import re
218    match = re.match(r'([^ ]*) >= [0-9\.]*', spec)
219    args = []
220    if match:
221        name = match.group(1)
222        args = [spec]
223    elif spec.find(' ') == -1:
224        name = spec
225    else:
226        Logs.error("Invalid package spec: %s" % spec)
227
228    found = None
229    pkg_name = name
230    args += kwargs.get('args', [])
231
232    if conf.env.PARDEBUG:
233        kwargs['mandatory'] = False  # Smash mandatory arg
234        found = conf.check_cfg(package=pkg_name + 'D',
235                               args=args + ['--cflags', '--libs'])
236        if found:
237            pkg_name += 'D'
238
239        args['mandatory'] = kwargs['mandatory']  # Unsmash mandatory arg
240
241    if not found:
242        found = conf.check_cfg(package=spec,
243                               args=args + ['--cflags', '--libs'],
244                               **kwargs)
245
246    if not conf.env.MSVC_COMPILER and 'system' in kwargs and kwargs['system']:
247        conf.env.append_unique('AUTOWAF_SYSTEM_PKGS', uselib_store)
248
249
250def normpath(path):
251    if sys.platform == 'win32':
252        return os.path.normpath(path).replace('\\', '/')
253    else:
254        return os.path.normpath(path)
255
256
257# Almost all GCC warnings common to C and C++
258gcc_common_warnings = [
259  # '-Waggregate-return', # Pretty esoteric, and not in clang
260  '-Waggressive-loop-optimizations',
261  '-Wall',
262  '-Walloc-zero',
263  '-Walloca',
264  # '-Walloca-larger-than=',
265  '-Wattribute-alias',
266  '-Wattributes',
267  '-Wbuiltin-declaration-mismatch',
268  '-Wbuiltin-macro-redefined',
269  '-Wcast-align',
270  '-Wcast-align=strict',
271  '-Wcast-qual',
272  '-Wconversion',
273  '-Wcoverage-mismatch',
274  '-Wcpp',
275  '-Wdate-time',
276  '-Wdeprecated',
277  '-Wdeprecated-declarations',
278  '-Wdisabled-optimization',
279  '-Wdiv-by-zero',
280  '-Wdouble-promotion',
281  '-Wduplicated-branches',
282  '-Wduplicated-cond',
283  '-Wextra',
284  '-Wfloat-equal',
285  '-Wformat-signedness',
286  '-Wnormalized',
287  # '-Wframe-larger-than=',
288  '-Wfree-nonheap-object',
289  '-Whsa',
290  '-Wif-not-aligned',
291  '-Wignored-attributes',
292  '-Winline',
293  '-Wint-to-pointer-cast',
294  '-Winvalid-memory-model',
295  '-Winvalid-pch',
296  # '-Wlarger-than=',
297  '-Wlogical-op',
298  '-Wlto-type-mismatch',
299  '-Wmissing-declarations',
300  '-Wmissing-include-dirs',
301  '-Wmultichar',
302  '-Wnull-dereference',
303  '-Wodr',
304  '-Woverflow',
305  '-Wpacked',
306  '-Wpacked-bitfield-compat',
307  '-Wpadded',
308  '-Wpedantic',
309  '-Wpointer-compare',
310  '-Wpragmas',
311  '-Wredundant-decls',
312  '-Wreturn-local-addr',
313  '-Wscalar-storage-order',
314  '-Wshadow',
315  '-Wshift-count-negative',
316  '-Wshift-count-overflow',
317  '-Wshift-negative-value',
318  '-Wshift-overflow=2',
319  '-Wsizeof-array-argument',
320  '-Wstack-protector',
321  # '-Wstack-usage=',
322  '-Wstrict-aliasing',
323  '-Wstrict-overflow',
324  '-Wsuggest-attribute=cold',
325  '-Wsuggest-attribute=const',
326  '-Wsuggest-attribute=format',
327  '-Wsuggest-attribute=malloc',
328  '-Wsuggest-attribute=noreturn',
329  '-Wsuggest-attribute=pure',
330  '-Wswitch-bool',
331  '-Wnormalized',
332  # '-Wswitch-default', # Redundant with Wswitch and not in clang
333  '-Wswitch-enum',
334  '-Wswitch-unreachable',
335  '-Wsync-nand',
336  # '-Wsystem-headers',
337  '-Wtrampolines',
338  '-Wundef',
339  '-Wunused-macros',
340  '-Wunused-result',
341  '-Wvarargs',
342  '-Wvector-operation-performance',
343  '-Wvla',
344  # '-Wvla-larger-than=',
345  '-Wwrite-strings',
346]
347
348# Almost all C-specific GCC warnings, except those for ancient (pre-C99) C
349gcc_c_warnings = [
350  '-Wbad-function-cast',
351  '-Wc++-compat',
352  # '-Wc90-c99-compat',
353  '-Wc99-c11-compat',
354  # '-Wdeclaration-after-statement',
355  '-Wdesignated-init',
356  '-Wdiscarded-array-qualifiers',
357  '-Wdiscarded-qualifiers',
358  '-Wincompatible-pointer-types',
359  '-Wint-conversion',
360  '-Wjump-misses-init',
361  '-Wmissing-prototypes',
362  '-Wnested-externs',
363  '-Wold-style-definition',
364  '-Woverride-init-side-effects',
365  '-Wpointer-to-int-cast',
366  '-Wstrict-prototypes',
367  # '-Wtraditional',
368  # '-Wtraditional-conversion',
369  # '-Wunsuffixed-float-constants',
370]
371
372# Almost all C++-specific GCC warnings, except those about common feature use
373gcc_cxx_warnings = [
374  '-Wconditionally-supported',
375  '-Wconversion-null',
376  '-Wctor-dtor-privacy',
377  '-Wdelete-incomplete',
378  '-Weffc++',
379  '-Wextra-semi',
380  '-Winherited-variadic-ctor',
381  '-Winvalid-offsetof',
382  '-Wliteral-suffix',
383  '-Wmultiple-inheritance',
384  # '-Wnamespaces',
385  '-Wnoexcept',
386  '-Wnon-template-friend',
387  '-Wnon-virtual-dtor',
388  '-Wold-style-cast',
389  '-Woverloaded-virtual',
390  '-Wplacement-new=2',
391  '-Wpmf-conversions',
392  '-Wregister',
393  '-Wsign-promo',
394  '-Wstrict-null-sentinel',
395  '-Wsubobject-linkage',
396  '-Wsuggest-final-methods',
397  '-Wsuggest-final-types',
398  '-Wsuggest-override',
399  '-Wsynth',
400  # '-Wtemplates',
401  '-Wterminate',
402  '-Wuseless-cast',
403  '-Wvirtual-inheritance',
404  '-Wvirtual-move-assign',
405  '-Wzero-as-null-pointer-constant',
406]
407
408
409def remove_all_warning_flags(env):
410    """Removes all warning flags except Werror or equivalent"""
411    if 'CC' in env:
412        if 'clang' in env.CC_NAME or 'gcc' in env.CC_NAME:
413            env['CFLAGS'] = [f for f in env['CFLAGS']
414                             if not (f.startswith('-W') and f != '-Werror')]
415        elif 'msvc' in env.CC_NAME:
416            env['CFLAGS'] = [f for f in env['CFLAGS']
417                             if not (f.startswith('/W') and f != '/WX')]
418
419    if 'CXX' in env:
420        if 'clang' in env.CXX_NAME or 'gcc' in env.CXX_NAME:
421            env['CXXFLAGS'] = [f for f in env['CXXFLAGS']
422                               if not (f.startswith('-W') and f != '-Werror')]
423        elif 'msvc' in env.CXX_NAME:
424            env['CXXFLAGS'] = [f for f in env['CXXFLAGS']
425                               if not (f.startswith('/W') and f != '/WX')]
426
427
428def enable_all_warnings(env):
429    """Enables all known warnings"""
430    if 'CC' in env:
431        if 'clang' in env.CC_NAME:
432            env.append_unique('CFLAGS', ['-Weverything'])
433        elif 'gcc' in env.CC_NAME:
434            env.append_unique('CFLAGS', gcc_common_warnings)
435            env.append_unique('CFLAGS', gcc_c_warnings)
436        elif env.MSVC_COMPILER:
437            env.append_unique('CFLAGS', ['/Wall'])
438        else:
439            Logs.warn('Unknown compiler "%s", not enabling warnings' % env.CC_NAME)
440
441    if 'CXX' in env:
442        if 'clang' in env.CXX_NAME:
443            env.append_unique('CXXFLAGS', ['-Weverything',
444                                           '-Wno-c++98-compat',
445                                           '-Wno-c++98-compat-pedantic'])
446        elif 'gcc' in env.CXX_NAME:
447            env.append_unique('CXXFLAGS', gcc_common_warnings)
448            env.append_unique('CXXFLAGS', gcc_cxx_warnings)
449        elif env.MSVC_COMPILER:
450            env.append_unique('CXXFLAGS', ['/Wall'])
451        else:
452            Logs.warn('Unknown compiler "%s", not enabling warnings' % env.CXX_NAME)
453
454
455def set_warnings_as_errors(env):
456    if 'CC' in env:
457        if 'clang' in env.CC_NAME or 'gcc' in env.CC_NAME:
458            env.append_unique('CFLAGS', ['-Werror'])
459        elif env.MSVC_COMPILER:
460            env.append_unique('CFLAGS', ['/WX'])
461
462    if 'CXX' in env:
463        if 'clang' in env.CXX_NAME or 'gcc' in env.CXX_NAME:
464            env.append_unique('CXXFLAGS', ['-Werror'])
465        elif env.MSVC_COMPILER:
466            env.append_unique('CXXFLAGS', ['/WX'])
467
468
469def add_compiler_flags(env, lang, compiler_to_flags):
470    """Add compiler-specific flags, for example to suppress warnings.
471
472    The lang argument must be "c", "cxx", or "*" for both.
473
474    The compiler_to_flags argument must be a map from compiler name
475    ("clang", "gcc", or "msvc") to a list of command line flags.
476    """
477
478    if lang == "*":
479        add_compiler_flags(env, 'c', compiler_to_flags)
480        add_compiler_flags(env, 'cxx', compiler_to_flags)
481    else:
482        if lang == 'c':
483            compiler_name = env.CC_NAME
484        elif lang == 'cxx':
485            compiler_name = env.CXX_NAME
486        else:
487            raise Exception('Unknown language "%s"' % lang)
488
489        var_name = lang.upper() + 'FLAGS'
490        for name, flags in compiler_to_flags.items():
491            if name in compiler_name:
492                env.append_value(var_name, flags)
493
494
495def configure(conf):
496    def append_cxx_flags(flags):
497        conf.env.append_value('CFLAGS', flags)
498        conf.env.append_value('CXXFLAGS', flags)
499
500    if Options.options.docs:
501        conf.load('doxygen')
502
503    try:
504        conf.load('clang_compilation_database')
505    except Exception:
506        pass
507
508    prefix = normpath(os.path.abspath(os.path.expanduser(conf.env['PREFIX'])))
509
510    conf.env['DOCS'] = Options.options.docs and conf.env.DOXYGEN
511    conf.env['DEBUG'] = Options.options.debug or Options.options.pardebug
512    conf.env['PARDEBUG'] = Options.options.pardebug
513    conf.env['PREFIX'] = prefix
514
515    def config_dir(var, opt, default):
516        if opt:
517            conf.env[var] = normpath(opt)
518        else:
519            conf.env[var] = normpath(default)
520
521    opts = Options.options
522
523    config_dir('BINDIR',     opts.bindir,     os.path.join(prefix,  'bin'))
524    config_dir('SYSCONFDIR', opts.configdir,  os.path.join(prefix,  'etc'))
525    config_dir('DATADIR',    opts.datadir,    os.path.join(prefix,  'share'))
526    config_dir('INCLUDEDIR', opts.includedir, os.path.join(prefix,  'include'))
527    config_dir('LIBDIR',     opts.libdir,     os.path.join(prefix,  'lib'))
528
529    datadir = conf.env['DATADIR']
530    config_dir('MANDIR', opts.mandir, os.path.join(datadir, 'man'))
531    config_dir('DOCDIR', opts.docdir, os.path.join(datadir, 'doc'))
532
533    if Options.options.debug:
534        if conf.env['MSVC_COMPILER']:
535            conf.env['CFLAGS']    = ['/Od', '/Z7']
536            conf.env['CXXFLAGS']  = ['/Od', '/Z7']
537            conf.env['LINKFLAGS'] = ['/DEBUG', '/MANIFEST']
538        else:
539            conf.env['CFLAGS']   = ['-O0', '-g']
540            conf.env['CXXFLAGS'] = ['-O0', '-g']
541    else:
542        if 'CFLAGS' not in os.environ:
543            if conf.env['MSVC_COMPILER']:
544                conf.env.append_unique('CFLAGS', ['/O2', '/DNDEBUG'])
545            else:
546                conf.env.append_unique('CFLAGS', ['-O2', '-DNDEBUG'])
547
548        if 'CXXFLAGS' not in os.environ:
549            if conf.env['MSVC_COMPILER']:
550                conf.env.append_unique('CXXFLAGS', ['/O2', '/DNDEBUG'])
551            else:
552                conf.env.append_unique('CXXFLAGS', ['-O2', '-DNDEBUG'])
553
554    if conf.env['MSVC_COMPILER']:
555        conf.env['CFLAGS']   += ['/MD']
556        conf.env['CXXFLAGS'] += ['/MD']
557
558    if Options.options.ultra_strict:
559        Options.options.strict = True
560        remove_all_warning_flags(conf.env)
561        enable_all_warnings(conf.env)
562        if Options.options.werror and 'clang' in conf.env.CC_NAME:
563            conf.env.append_unique('CFLAGS', '-Wno-unknown-warning-option')
564        if Options.options.werror and 'clang' in conf.env.CXX_NAME:
565            conf.env.append_unique('CXXFLAGS', '-Wno-unknown-warning-option')
566
567    if conf.env.MSVC_COMPILER:
568        Options.options.no_coverage = True
569        append_cxx_flags(['/nologo',
570                          '/FS',
571                          '/D_CRT_SECURE_NO_WARNINGS',
572                          '/experimental:external',
573                          '/external:W0',
574                          '/external:anglebrackets'])
575        conf.env.append_unique('CXXFLAGS', ['/EHsc'])
576        conf.env.append_value('LINKFLAGS', '/nologo')
577    elif Options.options.strict:
578        if conf.env.DEST_OS != "darwin":
579            sanitizing = False
580            for f in conf.env.LINKFLAGS:
581                if f.startswith('-fsanitize'):
582                    sanitizing = True
583                    break;
584
585            if not sanitizing:
586                conf.env.append_value('LINKFLAGS', ['-Wl,--no-undefined'])
587
588            # Add less universal flags after checking they work
589            extra_flags = ['-Wlogical-op',
590                           '-Wsuggest-attribute=noreturn',
591                           '-Wunsafe-loop-optimizations']
592            cflags = flag_check_flags(conf, conf.env.CFLAGS) + extra_flags
593            if conf.check_cc(cflags=cflags,
594                             mandatory=False,
595                             msg="Checking for extra C warning flags"):
596                conf.env.append_value('CFLAGS', extra_flags)
597            if 'COMPILER_CXX' in conf.env:
598                cxxflags = flag_check_flags(conf, conf.env.CXXFLAGS) + extra_flags
599                if conf.check_cxx(cxxflags=cxxflags,
600                                  mandatory=False,
601                                  msg="Checking for extra C++ warning flags"):
602                    conf.env.append_value('CXXFLAGS', extra_flags)
603
604    if not conf.env['MSVC_COMPILER']:
605        append_cxx_flags(['-fshow-column'])
606
607    if Options.options.werror:
608        if conf.env.MSVC_COMPILER:
609            append_cxx_flags('/WX')
610        else:
611            append_cxx_flags('-Werror')
612
613    conf.env.NO_COVERAGE = True
614    conf.env.BUILD_TESTS = False
615    try:
616        conf.env.BUILD_TESTS = Options.options.build_tests
617        conf.env.NO_COVERAGE = Options.options.no_coverage
618        if conf.env.BUILD_TESTS and not Options.options.no_coverage:
619            # Set up unit test code coverage
620            if conf.is_defined('CLANG'):
621                for cov in [conf.env.CC[0].replace('clang', 'llvm-cov'),
622                            'llvm-cov']:
623                    if conf.find_program(cov, var='LLVM_COV', mandatory=False):
624                        break
625            else:
626                if 'CC' in conf.env:
627                    if conf.check_cc(cflags=check_flags(conf, conf.env.CFLAGS),
628                                lib='gcov',
629                                mandatory=False,
630                                uselib_store='GCOV'):
631                        conf.env.HAVE_GCOV = True
632                else:
633                    if conf.check_cxx(cflags=check_flags(conf, conf.env.CXXFLAGS),
634                                 lib='gcov',
635                                 mandatory=False,
636                                 uselib_store='GCOV'):
637                        conf.env.HAVE_GCOV = True
638    except AttributeError:
639        pass # Test options do not exist
640    except Exception as e:
641        Logs.error("error: %s" % e)
642
643    # Define version in configuration
644    appname = getattr(Context.g_module, Context.APPNAME, 'noname')
645    version = getattr(Context.g_module, Context.VERSION, '0.0.0')
646    defname = appname.upper().replace('-', '_').replace('.', '_')
647    conf.define(defname + '_VERSION', version)
648    conf.env[defname + '_VERSION'] = version
649
650
651def display_summary(conf, msgs=None):
652    if len(conf.stack_path) == 1:
653        display_msg(conf, "Install prefix", conf.env['PREFIX'])
654        if 'COMPILER_CC' in conf.env:
655            display_msg(conf, "C Flags", ' '.join(conf.env['CFLAGS']))
656        if 'COMPILER_CXX' in conf.env:
657            display_msg(conf, "C++ Flags", ' '.join(conf.env['CXXFLAGS']))
658        display_msg(conf, "Debuggable", bool(conf.env['DEBUG']))
659        display_msg(conf, "Build documentation", bool(conf.env['DOCS']))
660
661    if msgs is not None:
662        display_msgs(conf, msgs)
663
664
665def check_flags(conf, flags):
666    if conf.env.MSVC_COMPILER:
667        return []
668
669    # Disable silly attribute warnings that trigger in the generated check code
670    result = []
671    if '-Wsuggest-attribute=const' in flags:
672        result += ['-Wno-suggest-attribute=const']
673    if '-Wsuggest-attribute=pure' in flags:
674        result += ['-Wno-suggest-attribute=pure']
675
676    return result
677
678
679def flag_check_flags(conf, flags):
680    if conf.env.MSVC_COMPILER:
681        return ['/WX'] + check_flags(conf, flags)
682    else:
683        return ['-Werror'] + check_flags(conf, flags)
684
685
686def set_c_lang(conf, lang, **kwargs):
687    "Set a specific C language standard, like 'c99' or 'c11'"
688    if conf.env.MSVC_COMPILER:
689        # MSVC has no hope or desire to compile C99, just compile as C++
690        conf.env.append_unique('CFLAGS', ['/TP'])
691        return True
692    elif not (lang == 'c99' and '-std=c11' in conf.env.CFLAGS):
693        flag = '-std=%s' % lang
694        if conf.check(features='c cstlib',
695                      cflags=flag_check_flags(conf, conf.env.CFLAGS) + [flag],
696                      msg="Checking for flag '%s'" % flag,
697                      **kwargs):
698            conf.env.append_unique('CFLAGS', [flag])
699            return True
700        return False
701
702
703def set_cxx_lang(conf, lang):
704    "Set a specific C++ language standard, like 'c++11', 'c++14', or 'c++17'"
705    if conf.env.MSVC_COMPILER:
706        if lang != 'c++14':
707            lang = 'c++latest'
708        conf.env.append_unique('CXXFLAGS', ['/std:%s' % lang])
709    else:
710        flag = '-std=%s' % lang
711        conf.check(cxxflags=flag_check_flags(conf, conf.env.CXXFLAGS) + [flag],
712                   msg="Checking for flag '%s'" % flag)
713        conf.env.append_unique('CXXFLAGS', [flag])
714
715
716def set_modern_c_flags(conf):
717    "Use the most modern C language available"
718    if 'COMPILER_CC' in conf.env:
719        if conf.env.MSVC_COMPILER:
720            # MSVC has no hope or desire to compile C99, just compile as C++
721            conf.env.append_unique('CFLAGS', ['/TP'])
722        else:
723            for flag in ['-std=c11', '-std=c99']:
724                if conf.check(cflags=['-Werror', flag], mandatory=False,
725                              msg="Checking for flag '%s'" % flag):
726                    conf.env.append_unique('CFLAGS', [flag])
727                    break
728
729
730def set_modern_cxx_flags(conf, mandatory=False):
731    "Use the most modern C++ language available"
732    if 'COMPILER_CXX' in conf.env:
733        if conf.env.MSVC_COMPILER:
734            conf.env.append_unique('CXXFLAGS', ['/std:c++latest'])
735        else:
736            for lang in ['c++14', 'c++1y', 'c++11', 'c++0x']:
737                flag = '-std=%s' % lang
738                if conf.check(cxxflags=['-Werror', flag], mandatory=False,
739                              msg="Checking for flag '%s'" % flag):
740                    conf.env.append_unique('CXXFLAGS', [flag])
741                    break
742
743
744def set_local_lib(conf, name, has_objects):
745    var_name = 'HAVE_' + nameify(name.upper())
746    conf.define(var_name, 1)
747    conf.env[var_name] = 1
748    if has_objects:
749        if type(conf.env['AUTOWAF_LOCAL_LIBS']) != dict:
750            conf.env['AUTOWAF_LOCAL_LIBS'] = {}
751        conf.env['AUTOWAF_LOCAL_LIBS'][name.lower()] = True
752    else:
753        if type(conf.env['AUTOWAF_LOCAL_HEADERS']) != dict:
754            conf.env['AUTOWAF_LOCAL_HEADERS'] = {}
755        conf.env['AUTOWAF_LOCAL_HEADERS'][name.lower()] = True
756
757
758def append_property(obj, key, val):
759    if hasattr(obj, key):
760        setattr(obj, key, getattr(obj, key) + val)
761    else:
762        setattr(obj, key, val)
763
764
765@feature('c', 'cxx')
766@before('apply_link')
767def version_lib(self):
768    if self.env.DEST_OS == 'win32':
769        self.vnum = None  # Prevent waf from automatically appending -0
770    if self.env['PARDEBUG']:
771        applicable = ['cshlib', 'cxxshlib', 'cstlib', 'cxxstlib']
772        if [x for x in applicable if x in self.features]:
773            self.target = self.target + 'D'
774
775
776def set_lib_env(conf,
777                name,
778                version,
779                has_objects=True,
780                include_path=None,
781                lib_path=None,
782                lib=None):
783    "Set up environment for local library as if found via pkg-config."
784    NAME         = name.upper()
785    major_ver    = version.split('.')[0]
786    pkg_var_name = 'PKG_' + name.replace('-', '_') + '_' + major_ver
787    lib_name     = '%s-%s' % (lib if lib is not None else name, major_ver)
788
789    if lib_path is None:
790        lib_path = str(conf.path.get_bld())
791
792    if include_path is None:
793        include_path = str(conf.path)
794
795    if conf.env.PARDEBUG:
796        lib_name += 'D'
797
798    conf.env[pkg_var_name]       = lib_name
799    conf.env['INCLUDES_' + NAME] = [include_path]
800    conf.env['LIBPATH_' + NAME]  = [lib_path]
801    if has_objects:
802        conf.env['LIB_' + NAME] = [lib_name]
803
804    conf.run_env.append_unique(lib_path_name, [lib_path])
805    conf.define(NAME + '_VERSION', version)
806
807
808def display_msg(conf, msg, status=None, color=None):
809    color = 'CYAN'
810    if type(status) == bool and status:
811        color  = 'GREEN'
812        status = 'yes'
813    elif type(status) == bool and not status or status == "False":
814        color  = 'YELLOW'
815        status = 'no'
816    Logs.pprint('BOLD', '%s' % msg.ljust(conf.line_just), sep='')
817    Logs.pprint('BOLD', ":", sep='')
818    Logs.pprint(color, status)
819
820
821def display_msgs(conf, msgs):
822    for k, v in msgs.items():
823        display_msg(conf, k, v)
824
825
826def link_flags(env, lib):
827    return ' '.join(map(lambda x: env['LIB_ST'] % x,
828                        env['LIB_' + lib]))
829
830
831def compile_flags(env, lib):
832    return ' '.join(map(lambda x: env['CPPPATH_ST'] % x,
833                        env['INCLUDES_' + lib]))
834
835
836def build_pc(bld, name, version, version_suffix, libs, subst_dict={}):
837    """Build a pkg-config file for a library.
838
839    name           -- uppercase variable name     (e.g. 'SOMENAME')
840                      or path to template without .pc.in extension
841    version        -- version string              (e.g. '1.2.3')
842    version_suffix -- name version suffix         (e.g. '2')
843    libs           -- string/list of dependencies (e.g. 'LIBFOO GLIB')
844    """
845
846    if '/' in name:
847        source = '%s.pc.in' % name.lower()
848        name = os.path.basename(name)
849    else:
850        source = '%s.pc.in' % name.lower()
851
852    pkg_prefix       = bld.env['PREFIX']
853    if len(pkg_prefix) > 1 and pkg_prefix[-1] == '/':
854        pkg_prefix = pkg_prefix[:-1]
855
856    target = name.lower()
857    if version_suffix != '':
858        target += '-' + version_suffix
859
860    if bld.env['PARDEBUG']:
861        target += 'D'
862
863    target += '.pc'
864
865    libdir = bld.env['LIBDIR']
866    if libdir.startswith(pkg_prefix):
867        libdir = libdir.replace(pkg_prefix, '${exec_prefix}')
868
869    includedir = bld.env['INCLUDEDIR']
870    if includedir.startswith(pkg_prefix):
871        includedir = includedir.replace(pkg_prefix, '${prefix}')
872
873    obj = bld(features='subst',
874              source=source,
875              target=target,
876              install_path=os.path.join(bld.env['LIBDIR'], 'pkgconfig'),
877              exec_prefix='${prefix}',
878              PREFIX=pkg_prefix,
879              EXEC_PREFIX='${prefix}',
880              LIBDIR=libdir,
881              INCLUDEDIR=includedir)
882
883    if type(libs) != list:
884        libs = libs.split()
885
886    subst_dict[name + '_VERSION'] = version
887    subst_dict[name + '_MAJOR_VERSION'] = version[0:version.find('.')]
888    for i in libs:
889        subst_dict[i + '_LIBS']   = link_flags(bld.env, i)
890        lib_cflags = compile_flags(bld.env, i)
891        if lib_cflags == '':
892            lib_cflags = ' '
893        subst_dict[i + '_CFLAGS'] = lib_cflags
894
895    obj.__dict__.update(subst_dict)
896
897
898def build_dox(bld,
899              name,
900              version,
901              srcdir,
902              blddir,
903              outdir='',
904              versioned=True,
905              install_man=True):
906    """Build Doxygen API documentation"""
907    if not bld.env['DOCS']:
908        return
909
910    # Doxygen paths in are relative to the doxygen file
911    src_dir = bld.path.srcpath()
912    subst_tg = bld(features='subst',
913                   source='doc/reference.doxygen.in',
914                   target='doc/reference.doxygen',
915                   install_path='',
916                   name='doxyfile')
917
918    subst_dict = {
919        name + '_VERSION': version,
920        name + '_SRCDIR': os.path.abspath(src_dir),
921        name + '_DOC_DIR': ''
922    }
923
924    subst_tg.__dict__.update(subst_dict)
925
926    subst_tg.post()
927
928    docs = bld(features='doxygen',
929               doxyfile='doc/reference.doxygen')
930
931    docs.post()
932
933    outname = name.lower()
934    if versioned:
935        outname += '-%d' % int(version[0:version.find('.')])
936    bld.install_files(
937        os.path.join('${DOCDIR}', outname, outdir, 'html'),
938        bld.path.get_bld().ant_glob('doc/html/*'))
939
940    if install_man:
941        for i in range(1, 8):
942            bld.install_files(
943                '${MANDIR}/man%d' % i,
944                bld.path.get_bld().ant_glob('doc/man/man%d/*' % i,
945                                            excl='**/_*'))
946
947
948def build_version_files(header_path, source_path, domain, major, minor, micro):
949    """Generate version code header"""
950    header_path = os.path.abspath(header_path)
951    source_path = os.path.abspath(source_path)
952    text  = "int " + domain + "_major_version = " + str(major) + ";\n"
953    text += "int " + domain + "_minor_version = " + str(minor) + ";\n"
954    text += "int " + domain + "_micro_version = " + str(micro) + ";\n"
955    try:
956        o = open(source_path, 'w')
957        o.write(text)
958        o.close()
959    except IOError:
960        Logs.error('Failed to open %s for writing\n' % source_path)
961        sys.exit(-1)
962
963    text  = "#ifndef __" + domain + "_version_h__\n"
964    text += "#define __" + domain + "_version_h__\n"
965    text += "extern const char* " + domain + "_revision;\n"
966    text += "extern int " + domain + "_major_version;\n"
967    text += "extern int " + domain + "_minor_version;\n"
968    text += "extern int " + domain + "_micro_version;\n"
969    text += "#endif /* __" + domain + "_version_h__ */\n"
970    try:
971        o = open(header_path, 'w')
972        o.write(text)
973        o.close()
974    except IOError:
975        Logs.warn('Failed to open %s for writing\n' % header_path)
976        sys.exit(-1)
977
978    return None
979
980
981def build_i18n_pot(bld, srcdir, dir, name, sources, copyright_holder=None):
982    Logs.info('Generating pot file from %s' % name)
983    pot_file = '%s.pot' % name
984
985    cmd = ['xgettext',
986           '--keyword=_',
987           '--keyword=N_',
988           '--keyword=S_',
989           '--from-code=UTF-8',
990           '-o', pot_file]
991
992    if copyright_holder:
993        cmd += ['--copyright-holder="%s"' % copyright_holder]
994
995    cmd += sources
996    Logs.info('Updating ' + pot_file)
997    subprocess.call(cmd, cwd=os.path.join(srcdir, dir))
998
999
1000def build_i18n_po(bld, srcdir, dir, name, sources, copyright_holder=None):
1001    pwd = os.getcwd()
1002    os.chdir(os.path.join(srcdir, dir))
1003    pot_file = '%s.pot' % name
1004    po_files = glob.glob('po/*.po')
1005    for po_file in po_files:
1006        cmd = ['msgmerge',
1007               '--update',
1008               po_file,
1009               pot_file]
1010        Logs.info('Updating ' + po_file)
1011        subprocess.call(cmd)
1012    os.chdir(pwd)
1013
1014
1015def build_i18n_mo(bld, srcdir, dir, name, sources, copyright_holder=None):
1016    pwd = os.getcwd()
1017    os.chdir(os.path.join(srcdir, dir))
1018    po_files = glob.glob('po/*.po')
1019    for po_file in po_files:
1020        mo_file = po_file.replace('.po', '.mo')
1021        cmd = ['msgfmt',
1022               '-c',
1023               '-f',
1024               '-o',
1025               mo_file,
1026               po_file]
1027        Logs.info('Generating ' + po_file)
1028        subprocess.call(cmd)
1029    os.chdir(pwd)
1030
1031
1032def build_i18n(bld, srcdir, dir, name, sources, copyright_holder=None):
1033    build_i18n_pot(bld, srcdir, dir, name, sources, copyright_holder)
1034    build_i18n_po(bld, srcdir, dir, name, sources, copyright_holder)
1035    build_i18n_mo(bld, srcdir, dir, name, sources, copyright_holder)
1036
1037
1038class ExecutionEnvironment:
1039    """Context that sets system environment variables for program execution"""
1040    def __init__(self, changes):
1041        self.original_environ = os.environ.copy()
1042
1043        self.diff = {}
1044        for path_name, paths in changes.items():
1045            value = os.pathsep.join(paths)
1046            if path_name in os.environ:
1047                value += os.pathsep + os.environ[path_name]
1048
1049            self.diff[path_name] = value
1050
1051        os.environ.update(self.diff)
1052
1053    def __str__(self):
1054        return '\n'.join(['%s="%s"' % (k, v) for k, v in self.diff.items()])
1055
1056    def __enter__(self):
1057        return self
1058
1059    def __exit__(self, type, value, traceback):
1060        os.environ = self.original_environ
1061
1062
1063class RunContext(Build.BuildContext):
1064    "runs an executable from the build directory"
1065    cmd = 'run'
1066
1067    def execute(self):
1068        self.restore()
1069        if not self.all_envs:
1070            self.load_envs()
1071
1072        with ExecutionEnvironment(self.env.AUTOWAF_RUN_ENV) as env:
1073            if Options.options.verbose:
1074                Logs.pprint('GREEN', str(env) + '\n')
1075
1076            if Options.options.cmd:
1077                Logs.pprint('GREEN', 'Running %s' % Options.options.cmd)
1078                subprocess.call(Options.options.cmd, shell=True)
1079            else:
1080                Logs.error("error: Missing --cmd option for run command")
1081
1082
1083def show_diff(from_lines, to_lines, from_filename, to_filename):
1084    import difflib
1085    import sys
1086
1087    same = True
1088    for line in difflib.unified_diff(
1089            from_lines, to_lines,
1090            fromfile=os.path.abspath(from_filename),
1091            tofile=os.path.abspath(to_filename)):
1092        sys.stderr.write(line)
1093        same = False
1094
1095    return same
1096
1097
1098def test_file_equals(patha, pathb):
1099    import filecmp
1100    import io
1101
1102    for path in (patha, pathb):
1103        if not os.access(path, os.F_OK):
1104            Logs.pprint('RED', 'error: missing file %s' % path)
1105            return False
1106
1107    if filecmp.cmp(patha, pathb, shallow=False):
1108        return True
1109
1110    with io.open(patha, 'rU', encoding='utf-8') as fa:
1111        with io.open(pathb, 'rU', encoding='utf-8') as fb:
1112            return show_diff(fa.readlines(), fb.readlines(), patha, pathb)
1113
1114
1115def bench_time():
1116    if hasattr(time, 'perf_counter'):  # Added in Python 3.3
1117        return time.perf_counter()
1118    else:
1119        return time.time()
1120
1121
1122class TestOutput:
1123    """Test output that is truthy if result is as expected"""
1124    def __init__(self, expected, result=None):
1125        self.stdout = self.stderr = None
1126        self.expected = expected
1127        self.result = result
1128
1129    def __bool__(self):
1130        return self.expected is None or self.result == self.expected
1131
1132    __nonzero__ = __bool__
1133
1134
1135def is_string(s):
1136    if sys.version_info[0] < 3:
1137        return isinstance(s, basestring)
1138    return isinstance(s, str)
1139
1140
1141class TestScope:
1142    """Scope for running tests that maintains pass/fail statistics"""
1143    def __init__(self, tst, name, defaults):
1144        self.tst = tst
1145        self.name = name
1146        self.defaults = defaults
1147        self.n_failed = 0
1148        self.n_total = 0
1149
1150    def run(self, test, **kwargs):
1151        if type(test) == list and 'name' not in kwargs:
1152            import pipes
1153            kwargs['name'] = ' '.join(map(pipes.quote, test))
1154
1155        if Options.options.test_filter and 'name' in kwargs:
1156            import re
1157            found = False
1158            for scope in self.tst.stack:
1159                if re.search(Options.options.test_filter, scope.name):
1160                    found = True
1161                    break
1162
1163            if (not found and
1164                not re.search(Options.options.test_filter, self.name) and
1165                not re.search(Options.options.test_filter, kwargs['name'])):
1166                return True
1167
1168        if callable(test):
1169            output = self._run_callable(test, **kwargs)
1170        elif type(test) == list:
1171            output = self._run_command(test, **kwargs)
1172        else:
1173            raise Exception("Unknown test type")
1174
1175        if not output:
1176            self.tst.log_bad('FAILED', kwargs['name'])
1177
1178        return self.tst.test_result(output)
1179
1180    def _run_callable(self, test, **kwargs):
1181        expected = kwargs['expected'] if 'expected' in kwargs else True
1182        return TestOutput(expected, test())
1183
1184    def _run_command(self, test, **kwargs):
1185        if 'stderr' in kwargs and kwargs['stderr'] == NONEMPTY:
1186            # Run with a temp file for stderr and check that it is non-empty
1187            import tempfile
1188            with tempfile.TemporaryFile() as stderr:
1189                kwargs['stderr'] = stderr
1190                output = self.run(test, **kwargs)
1191                stderr.seek(0, 2)  # Seek to end
1192                return (output if not output else
1193                        self.run(
1194                            lambda: stderr.tell() > 0,
1195                            name=kwargs['name'] + ' error message'))
1196
1197        try:
1198            # Run with stdout and stderr set to the appropriate streams
1199            out_stream = self._stream('stdout', kwargs)
1200            err_stream = self._stream('stderr', kwargs)
1201            return self._exec(test, **kwargs)
1202        finally:
1203            out_stream = out_stream.close() if out_stream else None
1204            err_stream = err_stream.close() if err_stream else None
1205
1206    def _stream(self, stream_name, kwargs):
1207        s = kwargs[stream_name] if stream_name in kwargs else None
1208        if is_string(s):
1209            kwargs[stream_name] = open(s, 'wb')
1210            return kwargs[stream_name]
1211        return None
1212
1213    def _exec(self,
1214              test,
1215              expected=0,
1216              name='',
1217              stdin=None,
1218              stdout=None,
1219              stderr=None,
1220              verbosity=1):
1221        import tempfile
1222
1223        def stream(s):
1224            return open(s, 'wb') if type(s) == str else s
1225
1226        if verbosity > 1:
1227            self.tst.log_good('RUN     ', name)
1228
1229        if Options.options.wrapper:
1230            import shlex
1231            test = shlex.split(Options.options.wrapper) + test
1232
1233        output = TestOutput(expected)
1234        with open(os.devnull, 'wb') as null:
1235            out = null if verbosity < 3 and not stdout else stdout
1236            tmp_err = None
1237            if stderr or verbosity >= 2:
1238                err = stderr
1239            else:
1240                tmp_err = tempfile.TemporaryFile()
1241                err = tmp_err
1242
1243            proc = subprocess.Popen(test, stdin=stdin, stdout=out, stderr=err)
1244            output.stdout, output.stderr = proc.communicate()
1245            output.result = proc.returncode
1246
1247            if tmp_err is not None:
1248                if output.result != expected:
1249                    tmp_err.seek(0)
1250                    for line in tmp_err:
1251                        sys.stderr.write(line.decode('utf-8'))
1252
1253                tmp_err.close()
1254
1255        if output and verbosity > 0:
1256            self.tst.log_good('      OK', name)
1257
1258        return output
1259
1260
1261class TestContext(Build.BuildContext):
1262    "runs test suite"
1263    fun = cmd = 'test'
1264
1265    def __init__(self, **kwargs):
1266        super(TestContext, self).__init__(**kwargs)
1267        self.start_time = bench_time()
1268        self.max_depth = 1
1269
1270        defaults = {'verbosity': Options.options.verbose}
1271        self.stack = [TestScope(self, Context.g_module.APPNAME, defaults)]
1272
1273    def defaults(self):
1274        return self.stack[-1].defaults
1275
1276    def finalize(self):
1277        if self.stack[-1].n_failed > 0:
1278            sys.exit(1)
1279
1280        super(TestContext, self).finalize()
1281
1282    def __call__(self, test, **kwargs):
1283        return self.stack[-1].run(test, **self.args(**kwargs))
1284
1285    def file_equals(self, from_path, to_path, **kwargs):
1286        kwargs.update({'expected': True,
1287                       'name': '%s == %s' % (from_path, to_path)})
1288        return self(lambda: test_file_equals(from_path, to_path), **kwargs)
1289
1290    def log_good(self, title, fmt, *args):
1291        Logs.pprint('GREEN', '[%s] %s' % (title.center(10), fmt % args))
1292
1293    def log_bad(self, title, fmt, *args):
1294        Logs.pprint('RED', '[%s] %s' % (title.center(10), fmt % args))
1295
1296    def pre_recurse(self, node):
1297        wscript_module = Context.load_module(node.abspath())
1298        group_name = wscript_module.APPNAME
1299        self.stack.append(TestScope(self, group_name, self.defaults()))
1300        self.max_depth = max(self.max_depth, len(self.stack) - 1)
1301
1302        bld_dir = node.get_bld().parent
1303
1304        if hasattr(wscript_module, 'test'):
1305            self.original_dir = os.getcwd()
1306            Logs.info("Waf: Entering directory `%s'", bld_dir)
1307            os.chdir(str(bld_dir))
1308
1309            parent_is_top = str(node.parent) == Context.top_dir
1310            if not self.env.NO_COVERAGE and parent_is_top:
1311                self.clear_coverage()
1312
1313            Logs.info('')
1314            self.log_good('=' * 10, 'Running %s tests\n', group_name)
1315
1316        super(TestContext, self).pre_recurse(node)
1317
1318    def test_result(self, success):
1319        self.stack[-1].n_total += 1
1320        self.stack[-1].n_failed += 1 if not success else 0
1321        return success
1322
1323    def pop(self):
1324        scope = self.stack.pop()
1325        self.stack[-1].n_total += scope.n_total
1326        self.stack[-1].n_failed += scope.n_failed
1327        return scope
1328
1329    def post_recurse(self, node):
1330        super(TestContext, self).post_recurse(node)
1331
1332        scope = self.pop()
1333        duration = (bench_time() - self.start_time) * 1000.0
1334        is_top = str(node.parent) == str(Context.top_dir)
1335
1336        wscript_module = Context.load_module(node.abspath())
1337        if not hasattr(wscript_module, 'test'):
1338            os.chdir(self.original_dir)
1339            return
1340
1341        Logs.info('')
1342        self.log_good('=' * 10, '%d tests from %s ran (%d ms total)',
1343                      scope.n_total, scope.name, duration)
1344
1345        if not self.env.NO_COVERAGE:
1346            if is_top:
1347                self.gen_coverage()
1348
1349            if os.path.exists('coverage/index.html'):
1350                self.log_good('REPORT', '<file://%s>',
1351                              os.path.abspath('coverage/index.html'))
1352
1353        successes = scope.n_total - scope.n_failed
1354        Logs.pprint('GREEN', '[  PASSED  ] %d tests' % successes)
1355        if scope.n_failed > 0:
1356            Logs.pprint('RED', '[  FAILED  ] %d tests' % scope.n_failed)
1357
1358        Logs.info("\nWaf: Leaving directory `%s'" % os.getcwd())
1359        os.chdir(self.original_dir)
1360
1361    def execute(self):
1362        self.restore()
1363        if not self.all_envs:
1364            self.load_envs()
1365
1366        if not self.env.BUILD_TESTS:
1367            self.fatal('Configuration does not include tests')
1368
1369        with ExecutionEnvironment(self.env.AUTOWAF_RUN_ENV) as env:
1370            if self.defaults()['verbosity'] > 0:
1371                Logs.pprint('GREEN', str(env) + '\n')
1372            self.recurse([self.run_dir])
1373
1374    def src_path(self, path):
1375        return os.path.relpath(os.path.join(str(self.path), path))
1376
1377    def args(self, **kwargs):
1378        all_kwargs = self.defaults().copy()
1379        all_kwargs.update(kwargs)
1380        return all_kwargs
1381
1382    def group(self, name, **kwargs):
1383        return TestGroup(
1384            self, self.stack[-1].name, name, **self.args(**kwargs))
1385
1386    def set_test_defaults(self, **kwargs):
1387        """Set default arguments to be passed to all tests"""
1388        self.stack[-1].defaults.update(kwargs)
1389
1390    def clear_coverage(self):
1391        """Zero old coverage data"""
1392        try:
1393            with open('cov-clear.log', 'w') as log:
1394                subprocess.call(['lcov', '-z', '-d', str(self.path)],
1395                                stdout=log, stderr=log)
1396
1397        except Exception as e:
1398            Logs.warn('Failed to run lcov to clear old coverage data (%s)' % e)
1399
1400    def gen_coverage(self):
1401        """Generate coverage data and report"""
1402        try:
1403            with open('cov.lcov', 'w') as out:
1404                with open('cov.log', 'w') as err:
1405                    subprocess.call(['lcov', '-c', '--no-external',
1406                                     '--rc', 'lcov_branch_coverage=1',
1407                                     '-b', '.',
1408                                     '-d', str(self.path)],
1409                                    stdout=out, stderr=err)
1410
1411            if not os.path.isdir('coverage'):
1412                os.makedirs('coverage')
1413
1414            with open('genhtml.log', 'w') as log:
1415                subprocess.call(['genhtml',
1416                                 '-o', 'coverage',
1417                                 '--rc', 'genhtml_branch_coverage=1',
1418                                 'cov.lcov'],
1419                                stdout=log, stderr=log)
1420
1421            summary = subprocess.check_output(
1422                ['lcov', '--summary',
1423                 '--rc', 'lcov_branch_coverage=1',
1424                 'cov.lcov'],
1425                stderr=subprocess.STDOUT).decode('ascii')
1426
1427            import re
1428            lines = re.search(r'lines\.*: (.*)%.*', summary).group(1)
1429            functions = re.search(r'functions\.*: (.*)%.*', summary).group(1)
1430            branches = re.search(r'branches\.*: (.*)%.*', summary).group(1)
1431            self.log_good(
1432                'COVERAGE', '%s%% lines, %s%% functions, %s%% branches',
1433                lines, functions, branches)
1434
1435        except Exception as e:
1436            Logs.warn('Failed to run lcov to generate coverage report (%s)')
1437
1438
1439class TestGroup:
1440    def __init__(self, tst, suitename, name, **kwargs):
1441        self.tst = tst
1442        self.suitename = suitename
1443        self.name = name
1444        self.kwargs = kwargs
1445        self.start_time = bench_time()
1446        tst.stack.append(TestScope(tst, name, tst.defaults()))
1447
1448    def label(self):
1449        return self.suitename + '.%s' % self.name if self.name else ''
1450
1451    def args(self, **kwargs):
1452        all_kwargs = self.tst.args(**self.kwargs)
1453        all_kwargs.update(kwargs)
1454        return all_kwargs
1455
1456    def __enter__(self):
1457        if 'verbosity' in self.kwargs and self.kwargs['verbosity'] > 0:
1458            self.tst.log_good('-' * 10, self.label())
1459        return self
1460
1461    def __call__(self, test, **kwargs):
1462        return self.tst(test, **self.args(**kwargs))
1463
1464    def file_equals(self, from_path, to_path, **kwargs):
1465        return self.tst.file_equals(from_path, to_path, **kwargs)
1466
1467    def __exit__(self, type, value, traceback):
1468        duration = (bench_time() - self.start_time) * 1000.0
1469        scope = self.tst.pop()
1470        n_passed = scope.n_total - scope.n_failed
1471        if scope.n_failed == 0:
1472            self.tst.log_good('-' * 10, '%d tests from %s (%d ms total)',
1473                              scope.n_total, self.label(), duration)
1474        else:
1475            self.tst.log_bad('-' * 10, '%d/%d tests from %s (%d ms total)',
1476                             n_passed, scope.n_total, self.label(), duration)
1477
1478
1479def run_ldconfig(ctx):
1480    should_run = (ctx.cmd == 'install' and
1481                  not ctx.env['RAN_LDCONFIG'] and
1482                  ctx.env['LIBDIR'] and
1483                  'DESTDIR' not in os.environ and
1484                  not Options.options.destdir)
1485
1486    if should_run:
1487        try:
1488            Logs.info("Waf: Running `/sbin/ldconfig %s'" % ctx.env['LIBDIR'])
1489            subprocess.call(['/sbin/ldconfig', ctx.env['LIBDIR']])
1490            ctx.env['RAN_LDCONFIG'] = True
1491        except Exception:
1492            pass
1493
1494
1495def run_script(cmds):
1496    for cmd in cmds:
1497        subprocess.check_call(cmd, shell=True)
1498