2 # -*- coding: utf-8 -*-
4 # Autowaf, useful waf utilities with support for recursive projects
5 # Copyright 2008-2011 David Robillard
7 # Licensed under the GNU GPL v2 or later, see COPYING file for details.
15 from waflib import Configure, Context, Logs, Node, Options, Task, Utils
16 from waflib.TaskGen import feature, before, after
21 # Only run autowaf hooks once (even if sub projects call several times)
25 # Compute dependencies globally
27 #preproc.go_absolute = True
30 @after('apply_incpaths')
31 def include_config_h(self):
32 self.env.append_value('INCPATHS', self.bld.bldnode.abspath())
34 def set_options(opt, debug_by_default=False):
35 "Add standard autowaf options if they havn't been added yet"
40 # Install directory options
41 dirs_options = opt.add_option_group('Installation directories', '')
43 # Move --prefix and --destdir to directory options group
44 for k in ('--prefix', '--destdir'):
45 option = opt.parser.get_option(k)
47 opt.parser.remove_option(k)
48 dirs_options.add_option(option)
50 # Standard directory options
51 dirs_options.add_option('--bindir', type='string',
52 help="Executable programs [Default: PREFIX/bin]")
53 dirs_options.add_option('--configdir', type='string',
54 help="Configuration data [Default: PREFIX/etc]")
55 dirs_options.add_option('--datadir', type='string',
56 help="Shared data [Default: PREFIX/share]")
57 dirs_options.add_option('--includedir', type='string',
58 help="Header files [Default: PREFIX/include]")
59 dirs_options.add_option('--libdir', type='string',
60 help="Libraries [Default: PREFIX/lib]")
61 dirs_options.add_option('--mandir', type='string',
62 help="Manual pages [Default: DATADIR/man]")
63 dirs_options.add_option('--docdir', type='string',
64 help="HTML documentation [Default: DATADIR/doc]")
68 opt.add_option('--optimize', action='store_false', default=True, dest='debug',
69 help="Build optimized binaries")
71 opt.add_option('--debug', action='store_true', default=False, dest='debug',
72 help="Build debuggable binaries")
74 opt.add_option('--pardebug', action='store_true', default=False, dest='pardebug',
75 help="Build parallel-installable debuggable libraries with D suffix")
77 opt.add_option('--grind', action='store_true', default=False, dest='grind',
78 help="Run tests in valgrind")
79 opt.add_option('--strict', action='store_true', default=False, dest='strict',
80 help="Use strict compiler flags and show all warnings")
81 opt.add_option('--ultra-strict', action='store_true', default=False, dest='ultra_strict',
82 help="Use even stricter compiler flags (likely to trigger many warnings in library headers)")
83 opt.add_option('--docs', action='store_true', default=False, dest='docs',
84 help="Build documentation - requires doxygen")
89 # a cross-platform utility for copying files as part of tasks
90 src = task.inputs[0].abspath()
91 tgt = task.outputs[0].abspath()
92 shutil.copy2 (src, tgt)
94 def check_header(conf, lang, name, define='', mandatory=True):
96 includes = '' # search default system include paths
97 if sys.platform == "darwin":
98 includes = '/opt/local/include'
101 check_func = conf.check_cc
103 check_func = conf.check_cxx
105 Logs.error("Unknown header language `%s'" % lang)
109 check_func(header_name=name, includes=includes,
110 define_name=define, mandatory=mandatory)
112 check_func(header_name=name, includes=includes, mandatory=mandatory)
115 return name.replace('/', '_').replace('++', 'PP').replace('-', '_').replace('.', '_')
117 def define(conf, var_name, value):
118 conf.define(var_name, value)
119 conf.env[var_name] = value
121 def check_pkg(conf, name, **args):
122 "Check for a package iff it hasn't been checked for yet"
123 if args['uselib_store'].lower() in conf.env['AUTOWAF_LOCAL_LIBS']:
128 var_name = 'CHECKED_' + nameify(args['uselib_store'])
129 check = not var_name in conf.env
130 mandatory = not 'mandatory' in args or args['mandatory']
131 if not check and 'atleast_version' in args:
132 # Re-check if version is newer than previous check
133 checked_version = conf.env['VERSION_' + name]
134 if checked_version and checked_version < args['atleast_version']:
136 if not check and mandatory and conf.env[var_name] == CheckType.OPTIONAL:
137 # Re-check if previous check was optional but this one is mandatory
141 pkg_var_name = 'PKG_' + name.replace('-', '_')
143 if conf.env.PARDEBUG:
144 args['mandatory'] = False # Smash mandatory arg
145 found = conf.check_cfg(package=pkg_name + 'D', args="--cflags --libs", **args)
149 args['mandatory'] = True # Unsmash mandatory arg
151 found = conf.check_cfg(package=pkg_name, args="--cflags --libs", **args)
153 conf.env[pkg_var_name] = pkg_name
154 if 'atleast_version' in args:
155 conf.env['VERSION_' + name] = args['atleast_version']
157 conf.env[var_name] = CheckType.MANDATORY
159 conf.env[var_name] = CheckType.OPTIONAL
163 if sys.platform == 'win32':
164 return os.path.normpath(path).replace('\\', '/')
166 return os.path.normpath(path)
168 def ensure_visible_symbols(bld, visible):
169 if bld.env['MSVC_COMPILER']:
171 print ('*** WARNING: MSVC does not allow symbols to be visible/exported by default while building ' + bld.name)
175 if not hasattr (bld,'cxxflags'):
177 if not hasattr (bld,'cflags'):
180 bld.cxxflags += [ '-fvisibility=default' ]
181 bld.cflags += [ '-fvisibility=default' ]
183 bld.cxxflags += [ '-fvisibility=hidden' ]
184 bld.cflags += [ '-fvisibility=hidden' ]
186 def set_basic_compiler_flags(conf, flag_dict):
187 if Options.options.debug:
188 conf.env.append_value('CFLAGS', flag_dict['debuggable'])
189 conf.env.append_value('CXXFLAGS', flag_dict['debuggable'])
190 conf.env.append_value('LINKFLAGS', flag_dict['linker-debuggable'])
192 conf.env.append_value('CFLAGS', flag_dict['nondebuggable'])
193 conf.env.append_value('CXXFLAGS', flag_dict['nondebuggable'])
195 if Options.options.ultra_strict:
196 Options.options.strict = True
197 conf.env.append_value('CFLAGS', flag_dict['ultra-strict'])
199 if Options.options.strict:
200 conf.env.append_value('CFLAGS', flag_dict['c-strict'])
201 conf.env.append_value('CXXFLAGS', flag_dict['cxx-strict'])
202 conf.env.append_value('CFLAGS', flag_dict['strict'])
203 conf.env.append_value('CXXFLAGS', flag_dict['strict'])
205 conf.env.append_value('CFLAGS', flag_dict['show-column'])
206 conf.env.append_value('CXXFLAGS', flag_dict['show-column'])
213 display_header('Global Configuration')
215 if Options.options.docs:
218 conf.env['DOCS'] = Options.options.docs
219 conf.env['DEBUG'] = Options.options.debug or Options.options.pardebug
220 conf.env['PARDEBUG'] = Options.options.pardebug
221 conf.env['PREFIX'] = normpath(os.path.abspath(os.path.expanduser(conf.env['PREFIX'])))
223 def config_dir(var, opt, default):
225 conf.env[var] = normpath(opt)
227 conf.env[var] = normpath(default)
229 opts = Options.options
230 prefix = conf.env['PREFIX']
232 config_dir('BINDIR', opts.bindir, os.path.join(prefix, 'bin'))
233 config_dir('SYSCONFDIR', opts.configdir, os.path.join(prefix, 'etc'))
234 config_dir('DATADIR', opts.datadir, os.path.join(prefix, 'share'))
235 config_dir('INCLUDEDIR', opts.includedir, os.path.join(prefix, 'include'))
236 config_dir('LIBDIR', opts.libdir, os.path.join(prefix, 'lib'))
237 config_dir('MANDIR', opts.mandir, os.path.join(conf.env['DATADIR'], 'man'))
238 config_dir('DOCDIR', opts.docdir, os.path.join(conf.env['DATADIR'], 'doc'))
240 if Options.options.docs:
241 doxygen = conf.find_program('doxygen')
243 conf.fatal("Doxygen is required to build with --docs")
245 dot = conf.find_program('dot')
247 conf.fatal("Graphviz (dot) is required to build with --docs")
249 conf.env.prepend_value('CFLAGS', '-I' + os.path.abspath('.'))
250 conf.env.prepend_value('CXXFLAGS', '-I' + os.path.abspath('.'))
252 display_msg(conf, "Install prefix", conf.env['PREFIX'])
253 display_msg(conf, "Debuggable build", str(conf.env['DEBUG']))
254 display_msg(conf, "Build documentation", str(conf.env['DOCS']))
259 def set_local_lib(conf, name, has_objects):
260 var_name = 'HAVE_' + nameify(name.upper())
261 define(conf, var_name, 1)
263 if type(conf.env['AUTOWAF_LOCAL_LIBS']) != dict:
264 conf.env['AUTOWAF_LOCAL_LIBS'] = {}
265 conf.env['AUTOWAF_LOCAL_LIBS'][name.lower()] = True
267 if type(conf.env['AUTOWAF_LOCAL_HEADERS']) != dict:
268 conf.env['AUTOWAF_LOCAL_HEADERS'] = {}
269 conf.env['AUTOWAF_LOCAL_HEADERS'][name.lower()] = True
271 def append_property(obj, key, val):
272 if hasattr(obj, key):
273 setattr(obj, key, getattr(obj, key) + val)
275 setattr(obj, key, val)
277 def use_lib(bld, obj, libs):
278 abssrcdir = os.path.abspath('.')
279 libs_list = libs.split()
281 in_headers = l.lower() in bld.env['AUTOWAF_LOCAL_HEADERS']
282 in_libs = l.lower() in bld.env['AUTOWAF_LOCAL_LIBS']
284 append_property(obj, 'use', ' lib%s ' % l.lower())
285 append_property(obj, 'framework', bld.env['FRAMEWORK_' + l])
286 if in_headers or in_libs:
287 inc_flag = '-iquote ' + os.path.join(abssrcdir, l.lower())
288 for f in ['CFLAGS', 'CXXFLAGS']:
289 if not inc_flag in bld.env[f]:
290 bld.env.prepend_value(f, inc_flag)
292 append_property(obj, 'uselib', ' ' + l)
295 @before('apply_link')
296 def version_lib(self):
297 if sys.platform == 'win32':
298 self.vnum = None # Prevent waf from automatically appending -0
299 if self.env['PARDEBUG']:
300 applicable = ['cshlib', 'cxxshlib', 'cstlib', 'cxxstlib']
301 if [x for x in applicable if x in self.features]:
302 self.target = self.target + 'D'
304 def set_lib_env(conf, name, version):
305 'Set up environment for local library as if found via pkg-config.'
307 major_ver = version.split('.')[0]
308 pkg_var_name = 'PKG_' + name.replace('-', '_')
309 lib_name = '%s-%s' % (name, major_ver)
310 if conf.env.PARDEBUG:
312 conf.env[pkg_var_name] = lib_name
313 conf.env['INCLUDES_' + NAME] = ['${INCLUDEDIR}/%s-%s' % (name, major_ver)]
314 conf.env['LIBPATH_' + NAME] = [conf.env.LIBDIR]
315 conf.env['LIB_' + NAME] = [lib_name]
317 def display_header(title):
318 Logs.pprint('BOLD', title)
320 def display_msg(conf, msg, status = None, color = None):
322 if type(status) == bool and status or status == "True":
324 elif type(status) == bool and not status or status == "False":
326 Logs.pprint('BOLD', " *", sep='')
327 Logs.pprint('NORMAL', "%s" % msg.ljust(conf.line_just - 3), sep='')
328 Logs.pprint('BOLD', ":", sep='')
329 Logs.pprint(color, status)
331 def link_flags(env, lib):
332 return ' '.join(map(lambda x: env['LIB_ST'] % x, env['LIB_' + lib]))
334 def compile_flags(env, lib):
335 return ' '.join(map(lambda x: env['CPPPATH_ST'] % x, env['INCLUDES_' + lib]))
346 def build_pc(bld, name, version, version_suffix, libs, subst_dict={}):
347 '''Build a pkg-config file for a library.
348 name -- uppercase variable name (e.g. 'SOMENAME')
349 version -- version string (e.g. '1.2.3')
350 version_suffix -- name version suffix (e.g. '2')
351 libs -- string/list of dependencies (e.g. 'LIBFOO GLIB')
353 pkg_prefix = bld.env['PREFIX']
354 if pkg_prefix[-1] == '/':
355 pkg_prefix = pkg_prefix[:-1]
357 target = name.lower()
358 if version_suffix != '':
359 target += '-' + version_suffix
361 if bld.env['PARDEBUG']:
366 libdir = bld.env['LIBDIR']
367 if libdir.startswith(pkg_prefix):
368 libdir = libdir.replace(pkg_prefix, '${exec_prefix}')
370 includedir = bld.env['INCLUDEDIR']
371 if includedir.startswith(pkg_prefix):
372 includedir = includedir.replace(pkg_prefix, '${prefix}')
374 obj = bld(features = 'subst',
375 source = '%s.pc.in' % name.lower(),
377 install_path = os.path.join(bld.env['LIBDIR'], 'pkgconfig'),
378 exec_prefix = '${prefix}',
380 EXEC_PREFIX = '${prefix}',
382 INCLUDEDIR = includedir)
384 if type(libs) != list:
387 subst_dict[name + '_VERSION'] = version
388 subst_dict[name + '_MAJOR_VERSION'] = version[0:version.find('.')]
390 subst_dict[i + '_LIBS'] = link_flags(bld.env, i)
391 lib_cflags = compile_flags(bld.env, i)
394 subst_dict[i + '_CFLAGS'] = lib_cflags
396 obj.__dict__.update(subst_dict)
398 def build_dir(name, subdir):
400 return os.path.join('build', name, subdir)
402 return os.path.join('build', subdir)
404 # Clean up messy Doxygen documentation after it is built
405 def make_simple_dox(name):
410 os.chdir(build_dir(name, 'doc/html'))
411 page = 'group__%s.html' % name
412 if not os.path.exists(page):
415 ['%s_API ' % NAME, ''],
416 ['%s_DEPRECATED ' % NAME, ''],
417 ['group__%s.html' % name, ''],
419 ['<script.*><\/script>', ''],
420 ['<hr\/><a name="details" id="details"><\/a><h2>.*<\/h2>', ''],
421 ['<link href=\"tabs.css\" rel=\"stylesheet\" type=\"text\/css\"\/>',
423 ['<img class=\"footer\" src=\"doxygen.png\" alt=\"doxygen\"\/>',
425 os.system("sed -i 's/%s/%s/g' %s" % (i[0], i[1], page))
426 os.rename('group__%s.html' % name, 'index.html')
427 for i in (glob.glob('*.png') +
428 glob.glob('*.html') +
431 if i != 'index.html' and i != 'style.css':
434 os.chdir(build_dir(name, 'doc/man/man3'))
435 for i in glob.glob('*.3'):
436 os.system("sed -i 's/%s_API //' %s" % (NAME, i))
437 for i in glob.glob('_*'):
440 except Exception as e:
441 Logs.error("Failed to fix up %s documentation: %s" % (name, e))
443 # Doxygen API documentation
444 def build_dox(bld, name, version, srcdir, blddir, outdir=''):
445 if not bld.env['DOCS']:
449 src_dir = os.path.join(srcdir, name.lower())
450 doc_dir = os.path.join(blddir, name.lower(), 'doc')
453 doc_dir = os.path.join(blddir, 'doc')
455 subst_tg = bld(features = 'subst',
456 source = 'doc/reference.doxygen.in',
457 target = 'doc/reference.doxygen',
462 name + '_VERSION' : version,
463 name + '_SRCDIR' : os.path.abspath(src_dir),
464 name + '_DOC_DIR' : os.path.abspath(doc_dir)
467 subst_tg.__dict__.update(subst_dict)
471 docs = bld(features = 'doxygen',
472 doxyfile = 'doc/reference.doxygen')
476 major = int(version[0:version.find('.')])
478 os.path.join('${DOCDIR}', '%s-%d' % (name.lower(), major), outdir, 'html'),
479 bld.path.get_bld().ant_glob('doc/html/*'))
480 for i in range(1, 8):
481 bld.install_files('${MANDIR}/man%d' % i,
482 bld.path.get_bld().ant_glob('doc/man/man%d/*' % i,
485 # Version code file generation
486 def build_version_files(header_path, source_path, domain, major, minor, micro, exportname, visheader):
487 header_path = os.path.abspath(header_path)
488 source_path = os.path.abspath(source_path)
489 text = "int " + domain + "_major_version = " + str(major) + ";\n"
490 text += "int " + domain + "_minor_version = " + str(minor) + ";\n"
491 text += "int " + domain + "_micro_version = " + str(micro) + ";\n"
493 o = open(source_path, 'w')
497 Logs.error('Failed to open %s for writing\n' % source_path)
500 text = "#ifndef __" + domain + "_version_h__\n"
501 text += "#define __" + domain + "_version_h__\n"
503 text += "#include \"" + visheader + "\"\n"
504 text += exportname + " extern const char* " + domain + "_revision;\n"
505 text += exportname + " extern int " + domain + "_major_version;\n"
506 text += exportname + " extern int " + domain + "_minor_version;\n"
507 text += exportname + " extern int " + domain + "_micro_version;\n"
508 text += "#endif /* __" + domain + "_version_h__ */\n"
510 o = open(header_path, 'w')
514 Logs.warn('Failed to open %s for writing\n' % header_path)
519 # Internationalization with gettext
520 def build_i18n_pot(bld, srcdir, dir, name, sources, copyright_holder=None):
521 Logs.info('Generating pot file from %s' % name)
522 pot_file = '%s.pot' % name
533 cmd += ['--copyright-holder="%s"' % copyright_holder]
536 Logs.info('Updating ' + pot_file)
537 subprocess.call(cmd, cwd=os.path.join(srcdir, dir))
539 def build_i18n_po(bld, srcdir, dir, name, sources, copyright_holder=None):
541 os.chdir(os.path.join(srcdir, dir))
542 pot_file = '%s.pot' % name
543 po_files = glob.glob('po/*.po')
544 for po_file in po_files:
547 '--no-fuzzy-matching',
550 Logs.info('Updating ' + po_file)
554 def build_i18n_mo(bld, srcdir, dir, name, sources, copyright_holder=None):
556 os.chdir(os.path.join(srcdir, dir))
557 pot_file = '%s.pot' % name
558 po_files = glob.glob('po/*.po')
559 for po_file in po_files:
560 mo_file = po_file.replace('.po', '.mo')
567 Logs.info('Generating ' + po_file)
571 def build_i18n(bld, srcdir, dir, name, sources, copyright_holder=None):
572 build_i18n_pot(bld, srcdir, dir, name, sources, copyright_holder)
573 build_i18n_po(bld, srcdir, dir, name, sources, copyright_holder)
574 build_i18n_mo(bld, srcdir, dir, name, sources, copyright_holder)
576 def cd_to_build_dir(ctx, appname):
577 orig_dir = os.path.abspath(os.curdir)
578 top_level = (len(ctx.stack_path) > 1)
580 os.chdir(os.path.join('build', appname))
583 Logs.pprint('GREEN', "Waf: Entering directory `%s'" % os.path.abspath(os.getcwd()))
585 def cd_to_orig_dir(ctx, child):
587 os.chdir(os.path.join('..', '..'))
591 def pre_test(ctx, appname, dirs=['src']):
594 diropts += ' -d ' + i
595 cd_to_build_dir(ctx, appname)
596 clear_log = open('lcov-clear.log', 'w')
599 # Clear coverage data
600 subprocess.call(('lcov %s -z' % diropts).split(),
601 stdout=clear_log, stderr=clear_log)
603 Logs.warn('Failed to run lcov, no coverage report will be generated')
607 def post_test(ctx, appname, dirs=['src'], remove=['*boost*', 'c++*']):
610 diropts += ' -d ' + i
611 coverage_log = open('lcov-coverage.log', 'w')
612 coverage_lcov = open('coverage.lcov', 'w')
613 coverage_stripped_lcov = open('coverage-stripped.lcov', 'w')
620 # Generate coverage data
621 subprocess.call(('lcov -c %s -b %s' % (diropts, base)).split(),
622 stdout=coverage_lcov, stderr=coverage_log)
624 # Strip unwanted stuff
626 ['lcov', '--remove', 'coverage.lcov'] + remove,
627 stdout=coverage_stripped_lcov, stderr=coverage_log)
629 # Generate HTML coverage output
630 if not os.path.isdir('coverage'):
631 os.makedirs('coverage')
632 subprocess.call('genhtml -o coverage coverage-stripped.lcov'.split(),
633 stdout=coverage_log, stderr=coverage_log)
636 Logs.warn('Failed to run lcov, no coverage report will be generated')
638 coverage_stripped_lcov.close()
639 coverage_lcov.close()
643 Logs.pprint('GREEN', "Waf: Leaving directory `%s'" % os.path.abspath(os.getcwd()))
644 top_level = (len(ctx.stack_path) > 1)
646 cd_to_orig_dir(ctx, top_level)
649 Logs.pprint('BOLD', 'Coverage:', sep='')
650 print('<file://%s>\n\n' % os.path.abspath('coverage/index.html'))
652 def run_tests(ctx, appname, tests, desired_status=0, dirs=['src'], name='*'):
656 diropts += ' -d ' + i
661 if type(i) == type([]):
664 Logs.pprint('BOLD', '** Test', sep='')
665 Logs.pprint('NORMAL', '%s' % s)
667 if Options.options.grind:
668 cmd = 'valgrind ' + i
669 if subprocess.call(cmd, shell=True) == desired_status:
670 Logs.pprint('GREEN', '** Pass')
673 Logs.pprint('RED', '** FAIL')
677 Logs.pprint('GREEN', '** Pass: All %s.%s tests passed' % (appname, name))
679 Logs.pprint('RED', '** FAIL: %d %s.%s tests failed' % (failures, appname, name))