# @begin:license # # Copyright (c) 2015-2021, Ben Niemann # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # @end:license import contextlib import fnmatch import logging import os.path import subprocess from waflib.Build import BuildContext, CFG_FILES from waflib.Configure import conf from waflib.Errors import BuildError, ConfigurationError from waflib import Logs from waflib import Utils top = '.' out = 'build' # Properly format command lines when using -v os.environ['WAF_CMD_FORMAT'] = 'string' @conf def pkg_config(ctx, store, package, minver): ctx.check_cfg( package=package, args=['%s >= %s' % (package, minver), '--cflags', '--libs'], uselib_store=store, msg="Checking for {} version >= {}".format(package, minver)) def options(ctx): ctx.load('compiler_cxx') ctx.load('qt5') grp = ctx.add_option_group('Test options') grp.parser.set_defaults(with_tests=True) grp.add_option( '--release', action='store_true', help="Build release version.") grp.add_option( '--with-tests', action='store_true', dest='with_tests', help="Enable tests.") grp.add_option( '--without-tests', action='store_false', dest='with_tests', help="Disable tests.") grp.add_option( '--with-coverage', action='store_true', default=False, help="Enable code coverage.") grp.add_option( '--test-lint', action='store_true', default=False, help="Run lint checks (mypy, pylint, ...).") grp.add_option( '--no-run-tests', dest='run_tests', action='store_false', default=True, help="Build, but do not run tests for './waf test'.") grp.add_option( '--test', default=None, help="Select specific test case to run (see pytest -k)") def configure(ctx): if ctx.options.with_coverage and not ctx.options.with_tests: raise ConfigurationError("--with-coverage requires --with-tests") ctx.load('compiler_cxx') ctx.qt5_vars = ['Qt5Core', 'Qt5Gui', 'Qt5Widgets', 'Qt5Svg', 'Qt5Quick', 'Qt5QuickControls2', 'Qt5Qml']; ctx.load('qt5') ctx.load('python') if not ctx.options.release: ctx.load('build_utils.waf.local_rpath', tooldir='.') ctx.load('build_utils.waf.static', tooldir='.') ctx.load('build_utils.waf.qml', tooldir='.') ctx.load('build_utils.waf.python', tooldir='.') ctx.load('build_utils.waf.cython', tooldir='.') ctx.load('build_utils.waf.flatbuffers', tooldir='.') ctx.env.DATADIR = os.path.join(ctx.env.PREFIX, 'share', 'noisicaa') ctx.env.LIBDIR = os.path.join(ctx.env.PREFIX, 'lib', 'noisicaa') ctx.env.append_value('CXXFLAGS', ['-O2', '-std=c++17']) ctx.env.append_value('CFLAGS', ['-O2']) ctx.env.append_value('LDFLAGS', ['-lrt']) ctx.env.append_value('INCLUDES', [ctx.srcnode.abspath(), ctx.bldnode.abspath()]) ctx.check_cfg(atleast_pkgconfig_version='0.29') ctx.pkg_config('JACK', 'jack', '1.9') ctx.pkg_config('FLATBUFFERS', 'flatbuffers', '2.0.0') ctx.pkg_config('SWRESAMPLE', 'libswresample', '1.2') ctx.pkg_config('AVUTIL', 'libavutil', '54') ctx.pkg_config('FMT', 'fmt', '7.1.3') ctx.pkg_config('PYTHON', 'python3', '3.8') ctx.check_python_module('flatbuffers', condition="ver >= num(2, 0)") ctx.check_python_module('eventfd') # has no __version__ ctx.find_program('faust') ctx.find_program('rsvg-convert', var='RSVG') if ctx.options.with_tests: ctx.env['ENABLE_TEST'] = True ctx.env.append_value('CXXFLAGS', ['-g', '-Wall']) ctx.env.append_value('CFLAGS', ['-g']) ctx.pkg_config('GTEST', 'gtest', '1.10') ctx.pkg_config('GMOCK', 'gmock', '1.10') ctx.check_python_module('pytest', condition="ver >= num(6, 2, 0)") ctx.check_python_module('pytest_cpp') # has no __version__ ctx.check_python_module('pytest_mypy') # has no __version__ ctx.check_python_module('pytest_pylint') # has no __version__ ctx.check_python_module('pytest_asyncio', condition="ver >= num(0, 16, 0)") ctx.check_python_module('pytestqt', condition="ver >= num(4, 0, 2)") ctx.find_program('pytest') else: ctx.env['ENABLE_TEST'] = False if ctx.options.with_coverage: ctx.start_msg("Enable code coverage analysis") ctx.env['ENABLE_COVERAGE'] = True ctx.env.append_value('CXXFLAGS', ['-fprofile-arcs', '-ftest-coverage']) ctx.env.append_value('CFLAGS', ['-fprofile-arcs', '-ftest-coverage']) ctx.env.append_value('LINKFLAGS', ['-lgcov', '-coverage']) ctx.end_msg('yes') ctx.check_python_module('pytest_cov', condition="ver >= num(3, 0, 0)") ctx.find_program('coverage-lcov') ctx.find_program('lcov') ctx.find_program('gcov') ctx.find_program('genhtml') else: ctx.env['ENABLE_COVERAGE'] = False ctx.recurse('3rdparty') create_config_py(ctx) def create_config_py(ctx): ctx.start_msg("Create noisicaa/config.py") node = ctx.bldnode.make_node('noisicaa/config.py') node.parent.mkdir() config = [] config.append("# Generated file, do not edit.") config.append("") with open(os.path.join(str(ctx.path), 'VERSION'), 'r') as fp: config.append('VERSION = %r' % fp.readline().strip()) config.append('DATA_DIR = %r' % ctx.env.DATADIR) config.append('LIB_DIR = %r' % ctx.env.LIBDIR) config.append("") defines = {} for k in ctx.env.DEFINES: a, _, b = k.partition('=') defines[a] = b for k in ctx.env['define_key']: caption = ctx.get_define_comment(k) if caption: caption = ' # %s' % caption try: value = defines[k] txt = '%s = %r%s' % (k, value, caption) except KeyError: txt = '# %s = UNSET%s' % (k, caption) config.append(txt) config.append("") node.write('\n'.join(config)) # config files must not be removed on "waf clean" ctx.env.append_unique(CFG_FILES, [node.abspath()]) ctx.end_msg('ok') def clear_coverage_data(ctx): if not ctx.env['ENABLE_COVERAGE']: return if os.path.exists(os.path.join(ctx.out_dir, '.coverage')): os.unlink(os.path.join(ctx.out_dir, '.coverage')) for dirpath, dirnames, filenames in os.walk(ctx.out_dir): for filename in filenames: if fnmatch.fnmatch(filename, '*.gcda'): os.unlink(os.path.join(dirpath, filename)) def coverage_report(ctx): if not ctx.env['ENABLE_COVERAGE']: return info_files = [] Logs.info("%sCollecting gcov data...", Logs.get_color('BLUE')) argv = [ ctx.env.LCOV[0], '--gcov-tool=' + ctx.env.GCOV[0], '--exclude=*_test.cpp', '--exclude=*_generated.h', '--exclude=*.fb.cc', '--exclude=*.fb.h', '--exclude=*.pb.cc', '--exclude=*.pb.h', '--exclude=*.pyx.cpp', '--no-external', '--capture', '--directory', ctx.top_dir, '-o', os.path.join(ctx.out_dir, 'lcov-cpp.info'), ] kw = { 'cwd': ctx.top_dir, 'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT, } ctx.log_command(argv, kw) rc, stdout, _ = Utils.run_process(argv, kw) if rc != 0: print(stdout.decode('utf-8')) raise BuildError() info_files.append('lcov-cpp.info') if os.path.exists(os.path.join(ctx.out_dir, '.coverage')): Logs.info("%sConverting coverage.py data...", Logs.get_color('BLUE')) argv = [ ctx.env.COVERAGE_LCOV[0], '--relative_path', '--data_file_path=' + os.path.join(ctx.out_dir, '.coverage'), '--output_file_path=' + os.path.join(ctx.out_dir, 'lcov-py.info'), ] kw = { 'cwd': ctx.out_dir, 'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT, } ctx.log_command(argv, kw) rc, stdout, _ = Utils.run_process(argv, kw) if rc != 0: print(stdout.decode('utf-8')) raise BuildError() info_files.append('lcov-py.info') Logs.info("%sMerging coverage data...", Logs.get_color('BLUE')) argv = [ctx.env.LCOV[0]] for f in info_files: argv += ['-a', f] argv += ['-o', 'lcov-merged.info'] kw = { 'cwd': ctx.out_dir, 'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT, } ctx.log_command(argv, kw) rc, stdout, _ = Utils.run_process(argv, kw) if rc != 0: print(stdout.decode('utf-8')) raise BuildError() Logs.info("%sGenerating coverage report...", Logs.get_color('BLUE')) report_dir = os.path.join(ctx.out_dir, 'coverage') os.makedirs(report_dir, exist_ok=True) argv = [ ctx.env.GENHTML[0], os.path.join(ctx.out_dir, 'lcov-merged.info'), '-o', report_dir, ] kw = { 'cwd': ctx.out_dir, 'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT, } ctx.log_command(argv, kw) rc, stdout, _ = Utils.run_process(argv, kw) if rc != 0: print(stdout.decode('utf-8')) raise BuildError() Logs.info("%sCoverage report: %sfile://%s/index.html", Logs.get_color('BLUE'), Logs.get_color('PINK'), report_dir) @conf @contextlib.contextmanager def group(ctx, grp): old_grp = ctx.current_group ctx.set_group(grp) try: yield finally: ctx.set_group(old_grp) @conf def in_group(ctx, grp): return ctx.get_group_name(ctx.current_group) == grp def run_tests(ctx): import pytest os.environ["COLUMNS"] = "80" argv = [ ctx.env.PYTEST[0], '-c', os.path.join(ctx.top_dir, 'etc', 'pytest.ini'), '-v', ] if Logs.verbose: argv += ['-s'] if ctx.options.test: argv += ['-k', ctx.options.test] if ctx.options.test_lint: argv += ['--mypy', '--pylint', '--pylint-rcfile=' + os.path.join(ctx.top_dir, 'etc', 'pylintrc')] if ctx.env.ENABLE_COVERAGE: argv += ['--cov=noisicaa', '--cov-config=' + os.path.join(ctx.top_dir, 'etc', 'coveragerc'), '--cov-report='] env = dict(os.environ) # Get e.g. the pipewire jack emulation out of the way. env['LD_LIBRARY_PATH'] = '' kw = { 'cwd': ctx.out_dir, 'env': env, } ctx.log_command(argv, kw) rc, _, _ = Utils.run_process(argv, kw) if rc != 0: raise BuildError() coverage_report(ctx) def build(ctx): if ctx.cmd == 'test' and not ctx.env.ENABLE_TEST: raise ConfigurationError("Not configured with --with-tests.") ctx.GRP_BUILD_TOOLS = 'build:tools' ctx.GRP_BUILD_GENERATED = 'build:generated' ctx.GRP_BUILD_MAIN = 'build:main' ctx.GRP_BUILD_TESTS = 'build:tests' ctx.add_group(ctx.GRP_BUILD_TOOLS) ctx.add_group(ctx.GRP_BUILD_GENERATED) ctx.add_group(ctx.GRP_BUILD_MAIN) ctx.add_group(ctx.GRP_BUILD_TESTS) ctx.set_group(ctx.GRP_BUILD_MAIN) ctx(rule='touch ${TGT}', target='.nobackup') with ctx.group(ctx.GRP_BUILD_TOOLS): ctx.recurse('build_utils') ctx.recurse('3rdparty') ctx.recurse('bin') ctx.recurse('data') ctx.recurse('noisicaa') if ctx.cmd == 'test' and ctx.options.run_tests: ctx.add_pre_fun(clear_coverage_data) ctx.add_post_fun(run_tests) class TestContext(BuildContext): """run unittests""" cmd = 'test'