| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2016-2021 The Meson development team | |
| from __future__ import annotations | |
| import subprocess | |
| import tempfile | |
| import textwrap | |
| import os | |
| import shutil | |
| import functools | |
| import threading | |
| import sys | |
| from itertools import chain | |
| from unittest import mock, skipIf, SkipTest, TestCase | |
| from pathlib import Path | |
| import typing as T | |
| import mesonbuild.mlog | |
| import mesonbuild.depfile | |
| import mesonbuild.dependencies.factory | |
| import mesonbuild.envconfig | |
| import mesonbuild.environment | |
| import mesonbuild.coredata | |
| import mesonbuild.modules.gnome | |
| from mesonbuild import mesonlib | |
| from mesonbuild import machinefile | |
| from mesonbuild.mesonlib import ( | |
| MachineChoice, is_windows, is_osx, is_cygwin, is_haiku, is_sunos | |
| ) | |
| from mesonbuild.compilers import ( | |
| detect_swift_compiler, compiler_from_language | |
| ) | |
| import mesonbuild.modules.pkgconfig | |
| from run_tests import ( | |
| Backend, | |
| get_fake_env | |
| ) | |
| from .baseplatformtests import BasePlatformTests | |
| from .helpers import * | |
| @functools.lru_cache() | |
| def is_real_gnu_compiler(path): | |
| ''' | |
| Check if the gcc we have is a real gcc and not a macOS wrapper around clang | |
| ''' | |
| if not path: | |
| return False | |
| out = subprocess.check_output([path, '--version'], encoding='utf-8', universal_newlines=True, stderr=subprocess.STDOUT) | |
| return 'Free Software Foundation' in out | |
| cross_dir = Path(__file__).parent.parent / 'cross' | |
| class MachineFileStoreTests(TestCase): | |
| def test_loading(self): | |
| store = machinefile.MachineFileStore([cross_dir / 'ubuntu-armhf.txt'], [], str(cross_dir)) | |
| self.assertIsNotNone(store) | |
| class NativeFileTests(BasePlatformTests): | |
| def setUp(self): | |
| super().setUp() | |
| self.testcase = os.path.join(self.unit_test_dir, '46 native file binary') | |
| self.current_config = 0 | |
| self.current_wrapper = 0 | |
| def helper_create_native_file(self, values: T.Dict[str, T.Dict[str, T.Union[str, int, float, bool, T.Sequence[T.Union[str, int, float, bool]]]]]) -> str: | |
| """Create a config file as a temporary file. | |
| values should be a nested dictionary structure of {section: {key: | |
| value}} | |
| """ | |
| filename = os.path.join(self.builddir, f'generated{self.current_config}.config') | |
| self.current_config += 1 | |
| with open(filename, 'wt', encoding='utf-8') as f: | |
| for section, entries in values.items(): | |
| f.write(f'[{section}]\n') | |
| for k, v in entries.items(): | |
| if isinstance(v, (bool, int, float)): | |
| f.write(f"{k}={v}\n") | |
| elif isinstance(v, str): | |
| f.write(f"{k}='{v}'\n") | |
| else: | |
| f.write("{}=[{}]\n".format(k, ', '.join([f"'{w}'" for w in v]))) | |
| return filename | |
| def helper_create_binary_wrapper(self, binary, dir_=None, extra_args=None, **kwargs): | |
| """Creates a wrapper around a binary that overrides specific values.""" | |
| filename = os.path.join(dir_ or self.builddir, f'binary_wrapper{self.current_wrapper}.py') | |
| extra_args = extra_args or {} | |
| self.current_wrapper += 1 | |
| if is_haiku(): | |
| chbang = '#!/bin/env python3' | |
| else: | |
| chbang = '#!/usr/bin/env python3' | |
| with open(filename, 'wt', encoding='utf-8') as f: | |
| f.write(textwrap.dedent('''\ | |
| {} | |
| import argparse | |
| import subprocess | |
| import sys | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| '''.format(chbang))) | |
| for name in chain(extra_args, kwargs): | |
| f.write(' parser.add_argument("-{0}", "--{0}", action="store_true")\n'.format(name)) | |
| f.write(' args, extra_args = parser.parse_known_args()\n') | |
| for name, value in chain(extra_args.items(), kwargs.items()): | |
| f.write(f' if args.{name}:\n') | |
| f.write(' print("{}", file=sys.{})\n'.format(value, kwargs.get('outfile', 'stdout'))) | |
| f.write(' sys.exit(0)\n') | |
| f.write(textwrap.dedent(''' | |
| ret = subprocess.run( | |
| ["{}"] + extra_args, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| print(ret.stdout.decode('utf-8')) | |
| print(ret.stderr.decode('utf-8'), file=sys.stderr) | |
| sys.exit(ret.returncode) | |
| if __name__ == '__main__': | |
| main() | |
| '''.format(binary))) | |
| if not is_windows(): | |
| os.chmod(filename, 0o755) | |
| return filename | |
| # On windows we need yet another level of indirection, as cmd cannot | |
| # invoke python files itself, so instead we generate a .bat file, which | |
| # invokes our python wrapper | |
| batfile = os.path.join(self.builddir, f'binary_wrapper{self.current_wrapper}.bat') | |
| with open(batfile, 'wt', encoding='utf-8') as f: | |
| f.write(fr'@{sys.executable} {filename} %*') | |
| return batfile | |
| def helper_for_compiler(self, lang, cb, for_machine = MachineChoice.HOST): | |
| """Helper for generating tests for overriding compilers for languages | |
| with more than one implementation, such as C, C++, ObjC, ObjC++, and D. | |
| """ | |
| env = get_fake_env() | |
| getter = lambda: compiler_from_language(env, lang, for_machine) | |
| cc = getter() | |
| binary, newid = cb(cc) | |
| env.binaries[for_machine].binaries[lang] = binary | |
| compiler = getter() | |
| self.assertEqual(compiler.id, newid) | |
| def test_multiple_native_files_override(self): | |
| wrapper = self.helper_create_binary_wrapper('bash', version='foo') | |
| config = self.helper_create_native_file({'binaries': {'bash': wrapper}}) | |
| wrapper = self.helper_create_binary_wrapper('bash', version='12345') | |
| config2 = self.helper_create_native_file({'binaries': {'bash': wrapper}}) | |
| self.init(self.testcase, extra_args=[ | |
| '--native-file', config, '--native-file', config2, | |
| '-Dcase=find_program']) | |
| # This test hangs on cygwin. | |
| @skipIf(os.name != 'posix' or is_cygwin(), 'Uses fifos, which are not available on non Unix OSes.') | |
| def test_native_file_is_pipe(self): | |
| fifo = os.path.join(self.builddir, 'native.file') | |
| os.mkfifo(fifo) | |
| with tempfile.TemporaryDirectory() as d: | |
| wrapper = self.helper_create_binary_wrapper('bash', d, version='12345') | |
| def filler(): | |
| with open(fifo, 'w', encoding='utf-8') as f: | |
| f.write('[binaries]\n') | |
| f.write(f"bash = '{wrapper}'\n") | |
| thread = threading.Thread(target=filler) | |
| thread.start() | |
| self.init(self.testcase, extra_args=['--native-file', fifo, '-Dcase=find_program']) | |
| thread.join() | |
| os.unlink(fifo) | |
| self.init(self.testcase, extra_args=['--wipe']) | |
| def test_multiple_native_files(self): | |
| wrapper = self.helper_create_binary_wrapper('bash', version='12345') | |
| config = self.helper_create_native_file({'binaries': {'bash': wrapper}}) | |
| wrapper = self.helper_create_binary_wrapper('python') | |
| config2 = self.helper_create_native_file({'binaries': {'python': wrapper}}) | |
| self.init(self.testcase, extra_args=[ | |
| '--native-file', config, '--native-file', config2, | |
| '-Dcase=find_program']) | |
| def _simple_test(self, case, binary, entry=None): | |
| wrapper = self.helper_create_binary_wrapper(binary, version='12345') | |
| config = self.helper_create_native_file({'binaries': {entry or binary: wrapper}}) | |
| self.init(self.testcase, extra_args=['--native-file', config, f'-Dcase={case}']) | |
| def test_find_program(self): | |
| self._simple_test('find_program', 'bash') | |
| def test_config_tool_dep(self): | |
| # Do the skip at this level to avoid screwing up the cache | |
| if mesonbuild.environment.detect_msys2_arch(): | |
| raise SkipTest('Skipped due to problems with LLVM on MSYS2') | |
| if not shutil.which('llvm-config'): | |
| raise SkipTest('No llvm-installed, cannot test') | |
| self._simple_test('config_dep', 'llvm-config') | |
| def test_python3_module(self): | |
| self._simple_test('python3', 'python3') | |
| def test_python_module(self): | |
| if is_windows(): | |
| # Bat adds extra crap to stdout, so the version check logic in the | |
| # python module breaks. This is fine on other OSes because they | |
| # don't need the extra indirection. | |
| raise SkipTest('bat indirection breaks internal sanity checks.') | |
| elif is_osx(): | |
| binary = 'python' | |
| else: | |
| binary = 'python2' | |
| # We not have python2, check for it | |
| for v in ['2', '2.7', '-2.7']: | |
| try: | |
| rc = subprocess.call(['pkg-config', '--cflags', f'python{v}'], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL) | |
| except FileNotFoundError: | |
| raise SkipTest('Not running Python 2 tests because pkg-config not found.') | |
| if rc == 0: | |
| break | |
| else: | |
| raise SkipTest('Not running Python 2 tests because dev packages not installed.') | |
| self._simple_test('python', binary, entry='python') | |
| @skipIf(is_windows(), 'Setting up multiple compilers on windows is hard') | |
| @skip_if_env_set('CC') | |
| def test_c_compiler(self): | |
| def cb(comp): | |
| if comp.id == 'gcc': | |
| if not shutil.which('clang'): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'clang', 'clang' | |
| if not is_real_gnu_compiler(shutil.which('gcc')): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'gcc', 'gcc' | |
| self.helper_for_compiler('c', cb) | |
| @skipIf(is_windows(), 'Setting up multiple compilers on windows is hard') | |
| @skip_if_env_set('CXX') | |
| def test_cpp_compiler(self): | |
| def cb(comp): | |
| if comp.id == 'gcc': | |
| if not shutil.which('clang++'): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'clang++', 'clang' | |
| if not is_real_gnu_compiler(shutil.which('g++')): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'g++', 'gcc' | |
| self.helper_for_compiler('cpp', cb) | |
| @skip_if_not_language('objc') | |
| @skip_if_env_set('OBJC') | |
| def test_objc_compiler(self): | |
| def cb(comp): | |
| if comp.id == 'gcc': | |
| if not shutil.which('clang'): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'clang', 'clang' | |
| if not is_real_gnu_compiler(shutil.which('gcc')): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'gcc', 'gcc' | |
| try: | |
| self.helper_for_compiler('objc', cb) | |
| except mesonlib.EnvironmentException as e: | |
| if 'GCC was not built with support for objective-c' in str(e): | |
| raise unittest.SkipTest("GCC doesn't support objective-c, test cannot run") | |
| raise | |
| @skip_if_not_language('objcpp') | |
| @skip_if_env_set('OBJCXX') | |
| def test_objcpp_compiler(self): | |
| def cb(comp): | |
| if comp.id == 'gcc': | |
| if not shutil.which('clang++'): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'clang++', 'clang' | |
| if not is_real_gnu_compiler(shutil.which('g++')): | |
| raise SkipTest('Only one compiler found, cannot test.') | |
| return 'g++', 'gcc' | |
| try: | |
| self.helper_for_compiler('objcpp', cb) | |
| except mesonlib.EnvironmentException as e: | |
| if 'GCC was not built with support for objective-c++' in str(e): | |
| raise unittest.SkipTest("G++ doesn't support objective-c++, test cannot run") | |
| raise | |
| @skip_if_not_language('d') | |
| @skip_if_env_set('DC') | |
| def test_d_compiler(self): | |
| def cb(comp): | |
| if comp.id == 'dmd': | |
| if shutil.which('ldc'): | |
| return 'ldc', 'ldc' | |
| elif shutil.which('gdc'): | |
| return 'gdc', 'gdc' | |
| else: | |
| raise SkipTest('No alternative dlang compiler found.') | |
| if shutil.which('dmd'): | |
| return 'dmd', 'dmd' | |
| raise SkipTest('No alternative dlang compiler found.') | |
| self.helper_for_compiler('d', cb) | |
| @skip_if_not_language('cs') | |
| @skip_if_env_set('CSC') | |
| def test_cs_compiler(self): | |
| def cb(comp): | |
| if comp.id == 'csc': | |
| if not shutil.which('mcs'): | |
| raise SkipTest('No alternate C# implementation.') | |
| return 'mcs', 'mcs' | |
| if not shutil.which('csc'): | |
| raise SkipTest('No alternate C# implementation.') | |
| return 'csc', 'csc' | |
| self.helper_for_compiler('cs', cb) | |
| @skip_if_not_language('fortran') | |
| @skip_if_env_set('FC') | |
| def test_fortran_compiler(self): | |
| def cb(comp): | |
| if comp.id == 'lcc': | |
| if shutil.which('lfortran'): | |
| return 'lfortran', 'lcc' | |
| raise SkipTest('No alternate Fortran implementation.') | |
| elif comp.id == 'gcc': | |
| if shutil.which('ifort'): | |
| # There is an ICC for windows (windows build, linux host), | |
| # but we don't support that ATM so let's not worry about it. | |
| if is_windows(): | |
| return 'ifort', 'intel-cl' | |
| return 'ifort', 'intel' | |
| elif shutil.which('flang'): | |
| return 'flang', 'flang' | |
| elif shutil.which('pgfortran'): | |
| return 'pgfortran', 'pgi' | |
| # XXX: there are several other fortran compilers meson | |
| # supports, but I don't have any of them to test with | |
| raise SkipTest('No alternate Fortran implementation.') | |
| if not shutil.which('gfortran'): | |
| raise SkipTest('No alternate Fortran implementation.') | |
| return 'gfortran', 'gcc' | |
| self.helper_for_compiler('fortran', cb) | |
| def _single_implementation_compiler(self, lang: str, binary: str, version_str: str, version: str) -> None: | |
| """Helper for languages with a single (supported) implementation. | |
| Builds a wrapper around the compiler to override the version. | |
| """ | |
| wrapper = self.helper_create_binary_wrapper(binary, version=version_str) | |
| env = get_fake_env() | |
| env.binaries.host.binaries[lang] = [wrapper] | |
| compiler = compiler_from_language(env, lang, MachineChoice.HOST) | |
| self.assertEqual(compiler.version, version) | |
| @skip_if_not_language('vala') | |
| @skip_if_env_set('VALAC') | |
| def test_vala_compiler(self): | |
| self._single_implementation_compiler( | |
| 'vala', 'valac', 'Vala 1.2345', '1.2345') | |
| @skip_if_not_language('rust') | |
| @skip_if_env_set('RUSTC') | |
| def test_rust_compiler(self): | |
| self._single_implementation_compiler( | |
| 'rust', 'rustc', 'rustc 1.2345', '1.2345') | |
| @skip_if_not_language('java') | |
| def test_java_compiler(self): | |
| self._single_implementation_compiler( | |
| 'java', 'javac', 'javac 9.99.77', '9.99.77') | |
| @skip_if_not_language('java') | |
| def test_java_classpath(self): | |
| if self.backend is not Backend.ninja: | |
| raise SkipTest('Jar is only supported with Ninja') | |
| testdir = os.path.join(self.unit_test_dir, '113 classpath') | |
| self.init(testdir) | |
| self.build() | |
| one_build_path = get_classpath(os.path.join(self.builddir, 'one.jar')) | |
| self.assertIsNone(one_build_path) | |
| two_build_path = get_classpath(os.path.join(self.builddir, 'two.jar')) | |
| self.assertEqual(two_build_path, 'one.jar') | |
| self.install() | |
| one_install_path = get_classpath(os.path.join(self.installdir, 'usr/bin/one.jar')) | |
| self.assertIsNone(one_install_path) | |
| two_install_path = get_classpath(os.path.join(self.installdir, 'usr/bin/two.jar')) | |
| self.assertIsNone(two_install_path) | |
| @skip_if_not_language('swift') | |
| def test_swift_compiler(self): | |
| wrapper = self.helper_create_binary_wrapper( | |
| 'swiftc', version='Swift 1.2345', outfile='stderr', | |
| extra_args={'Xlinker': 'macosx_version. PROJECT:ld - 1.2.3'}) | |
| env = get_fake_env() | |
| env.binaries.host.binaries['swift'] = [wrapper] | |
| compiler = detect_swift_compiler(env, MachineChoice.HOST) | |
| self.assertEqual(compiler.version, '1.2345') | |
| def test_native_file_dirs(self): | |
| testcase = os.path.join(self.unit_test_dir, '59 native file override') | |
| self.init(testcase, default_args=False, | |
| extra_args=['--native-file', os.path.join(testcase, 'nativefile')]) | |
| def test_native_file_dirs_overridden(self): | |
| testcase = os.path.join(self.unit_test_dir, '59 native file override') | |
| self.init(testcase, default_args=False, | |
| extra_args=['--native-file', os.path.join(testcase, 'nativefile'), | |
| '-Ddef_libdir=liblib', '-Dlibdir=liblib']) | |
| def test_compile_sys_path(self): | |
| """Compiling with a native file stored in a system path works. | |
| There was a bug which caused the paths to be stored incorrectly and | |
| would result in ninja invoking meson in an infinite loop. This tests | |
| for that by actually invoking ninja. | |
| """ | |
| testcase = os.path.join(self.common_test_dir, '1 trivial') | |
| # It really doesn't matter what's in the native file, just that it exists | |
| config = self.helper_create_native_file({'binaries': {'bash': 'false'}}) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| self.build() | |
| def test_user_options(self): | |
| testcase = os.path.join(self.common_test_dir, '40 options') | |
| for opt, value in [('testoption', 'some other val'), ('other_one', True), | |
| ('combo_opt', 'one'), ('array_opt', ['two']), | |
| ('integer_opt', 0), | |
| ('CaseSenSiTivE', 'SOME other Value'), | |
| ('CASESENSITIVE', 'some other Value')]: | |
| config = self.helper_create_native_file({'project options': {opt: value}}) | |
| with self.assertRaises(subprocess.CalledProcessError) as cm: | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option') | |
| def test_user_options_command_line_overrides(self): | |
| testcase = os.path.join(self.common_test_dir, '40 options') | |
| config = self.helper_create_native_file({'project options': {'other_one': True}}) | |
| self.init(testcase, extra_args=['--native-file', config, '-Dother_one=false']) | |
| def test_user_options_subproject(self): | |
| testcase = os.path.join(self.unit_test_dir, '78 user options for subproject') | |
| s = os.path.join(testcase, 'subprojects') | |
| if not os.path.exists(s): | |
| os.mkdir(s) | |
| s = os.path.join(s, 'sub') | |
| if not os.path.exists(s): | |
| sub = os.path.join(self.common_test_dir, '40 options') | |
| shutil.copytree(sub, s) | |
| for opt, value in [('testoption', 'some other val'), ('other_one', True), | |
| ('combo_opt', 'one'), ('array_opt', ['two']), | |
| ('integer_opt', 0)]: | |
| config = self.helper_create_native_file({'sub:project options': {opt: value}}) | |
| with self.assertRaises(subprocess.CalledProcessError) as cm: | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option') | |
| def test_option_bool(self): | |
| # Bools are allowed to be unquoted | |
| testcase = os.path.join(self.common_test_dir, '1 trivial') | |
| config = self.helper_create_native_file({'built-in options': {'werror': True}}) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| # Test that no-per subproject options are inherited from the parent | |
| if 'werror' in each['name']: | |
| self.assertEqual(each['value'], True) | |
| break | |
| else: | |
| self.fail('Did not find werror in build options?') | |
| def test_option_integer(self): | |
| # Bools are allowed to be unquoted | |
| testcase = os.path.join(self.common_test_dir, '1 trivial') | |
| config = self.helper_create_native_file({'built-in options': {'unity_size': 100}}) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| # Test that no-per subproject options are inherited from the parent | |
| if 'unity_size' in each['name']: | |
| self.assertEqual(each['value'], 100) | |
| break | |
| else: | |
| self.fail('Did not find unity_size in build options?') | |
| def test_builtin_options(self): | |
| testcase = os.path.join(self.common_test_dir, '2 cpp') | |
| config = self.helper_create_native_file({'built-in options': {'cpp_std': 'c++14'}}) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'cpp_std': | |
| self.assertEqual(each['value'], 'c++14') | |
| break | |
| else: | |
| self.fail('Did not find werror in build options?') | |
| def test_builtin_options_conf_overrides_env(self): | |
| testcase = os.path.join(self.common_test_dir, '2 cpp') | |
| config = self.helper_create_native_file({'built-in options': {'pkg_config_path': '/foo'}}) | |
| self.init(testcase, extra_args=['--native-file', config], override_envvars={'PKG_CONFIG_PATH': '/bar'}) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'pkg_config_path': | |
| self.assertEqual(each['value'], ['/foo']) | |
| break | |
| else: | |
| self.fail('Did not find pkg_config_path in build options?') | |
| def test_builtin_options_subprojects(self): | |
| testcase = os.path.join(self.common_test_dir, '98 subproject subdir') | |
| config = self.helper_create_native_file({'built-in options': {'default_library': 'both', 'c_args': ['-Dfoo']}, 'sub:built-in options': {'default_library': 'static'}}) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| found = 0 | |
| for each in configuration: | |
| # Test that no-per subproject options are inherited from the parent | |
| if 'c_args' in each['name']: | |
| # This path will be hit twice, once for build and once for host, | |
| self.assertEqual(each['value'], ['-Dfoo']) | |
| found += 1 | |
| elif each['name'] == 'default_library': | |
| self.assertEqual(each['value'], 'both') | |
| found += 1 | |
| elif each['name'] == 'sub:default_library': | |
| self.assertEqual(each['value'], 'static') | |
| found += 1 | |
| # FIXME: check that the subproject option has beeb added | |
| # into augments. | |
| self.assertEqual(found, 2, 'Did not find all two sections') | |
| def test_builtin_options_subprojects_overrides_buildfiles(self): | |
| # If the buildfile says subproject(... default_library: shared), ensure that's overwritten | |
| testcase = os.path.join(self.common_test_dir, '223 persubproject options') | |
| config = self.helper_create_native_file({'sub2:built-in options': {'default_library': 'shared'}}) | |
| with self.assertRaises((RuntimeError, subprocess.CalledProcessError)) as cm: | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| if isinstance(cm, RuntimeError): | |
| check = str(cm.exception) | |
| else: | |
| check = cm.exception.stdout | |
| self.assertIn(check, 'Parent should override default_library') | |
| def test_builtin_options_subprojects_dont_inherits_parent_override(self): | |
| # If the buildfile says subproject(... default_library: shared), ensure that's overwritten | |
| testcase = os.path.join(self.common_test_dir, '223 persubproject options') | |
| config = self.helper_create_native_file({'built-in options': {'default_library': 'both'}}) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| def test_builtin_options_compiler_properties(self): | |
| # the properties section can have lang_args, and those need to be | |
| # overwritten by the built-in options | |
| testcase = os.path.join(self.common_test_dir, '1 trivial') | |
| config = self.helper_create_native_file({ | |
| 'built-in options': {'c_args': ['-DFOO']}, | |
| 'properties': {'c_args': ['-DBAR']}, | |
| }) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'c_args': | |
| self.assertEqual(each['value'], ['-DFOO']) | |
| break | |
| else: | |
| self.fail('Did not find c_args in build options?') | |
| def test_builtin_options_compiler_properties_legacy(self): | |
| # The legacy placement in properties is still valid if a 'built-in | |
| # options' setting is present, but doesn't have the lang_args | |
| testcase = os.path.join(self.common_test_dir, '1 trivial') | |
| config = self.helper_create_native_file({ | |
| 'built-in options': {'default_library': 'static'}, | |
| 'properties': {'c_args': ['-DBAR']}, | |
| }) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'c_args': | |
| self.assertEqual(each['value'], ['-DBAR']) | |
| break | |
| else: | |
| self.fail('Did not find c_args in build options?') | |
| def test_builtin_options_paths(self): | |
| # the properties section can have lang_args, and those need to be | |
| # overwritten by the built-in options | |
| testcase = os.path.join(self.common_test_dir, '1 trivial') | |
| config = self.helper_create_native_file({ | |
| 'built-in options': {'bindir': 'foo'}, | |
| 'paths': {'bindir': 'bar'}, | |
| }) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'bindir': | |
| self.assertEqual(each['value'], 'foo') | |
| break | |
| else: | |
| self.fail('Did not find bindir in build options?') | |
| def test_builtin_options_paths_legacy(self): | |
| testcase = os.path.join(self.common_test_dir, '1 trivial') | |
| config = self.helper_create_native_file({ | |
| 'built-in options': {'default_library': 'static'}, | |
| 'paths': {'bindir': 'bar'}, | |
| }) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'bindir': | |
| self.assertEqual(each['value'], 'bar') | |
| break | |
| else: | |
| self.fail('Did not find bindir in build options?') | |
| @skip_if_not_language('rust') | |
| def test_bindgen_clang_arguments(self) -> None: | |
| if self.backend is not Backend.ninja: | |
| raise SkipTest('Rust is only supported with Ninja') | |
| testcase = os.path.join(self.rust_test_dir, '12 bindgen') | |
| config = self.helper_create_native_file({ | |
| 'properties': {'bindgen_clang_arguments': 'sentinel'} | |
| }) | |
| self.init(testcase, extra_args=['--native-file', config]) | |
| targets: T.List[T.Dict[str, T.Any]] = self.introspect('--targets') | |
| for t in targets: | |
| if t['id'].startswith('rustmod-bindgen'): | |
| args: T.List[str] = t['target_sources'][0]['compiler'] | |
| self.assertIn('sentinel', args, msg="Did not find machine file value") | |
| cargs_start = args.index('--') | |
| sent_arg = args.index('sentinel') | |
| self.assertLess(cargs_start, sent_arg, msg='sentinel argument does not come after "--"') | |
| break | |
| else: | |
| self.fail('Did not find a bindgen target') | |
| class CrossFileTests(BasePlatformTests): | |
| """Tests for cross file functionality not directly related to | |
| cross compiling. | |
| This is mainly aimed to testing overrides from cross files. | |
| """ | |
| def setUp(self): | |
| super().setUp() | |
| self.current_config = 0 | |
| self.current_wrapper = 0 | |
| def _cross_file_generator(self, *, needs_exe_wrapper: bool = False, | |
| exe_wrapper: T.Optional[T.List[str]] = None) -> str: | |
| if is_windows(): | |
| raise SkipTest('Cannot run this test on non-mingw/non-cygwin windows') | |
| return textwrap.dedent(f"""\ | |
| [binaries] | |
| c = '{shutil.which('gcc' if is_sunos() else 'cc')}' | |
| ar = '{shutil.which('ar')}' | |
| strip = '{shutil.which('strip')}' | |
| exe_wrapper = {str(exe_wrapper) if exe_wrapper is not None else '[]'} | |
| [properties] | |
| needs_exe_wrapper = {needs_exe_wrapper} | |
| [host_machine] | |
| system = 'linux' | |
| cpu_family = 'x86' | |
| cpu = 'i686' | |
| endian = 'little' | |
| """) | |
| def _stub_exe_wrapper(self) -> str: | |
| return textwrap.dedent('''\ | |
| #!/usr/bin/env python3 | |
| import subprocess | |
| import sys | |
| sys.exit(subprocess.run(sys.argv[1:]).returncode) | |
| ''') | |
| def test_needs_exe_wrapper_true(self): | |
| testdir = os.path.join(self.unit_test_dir, '70 cross test passed') | |
| with tempfile.TemporaryDirectory() as d: | |
| p = Path(d) / 'crossfile' | |
| with p.open('wt', encoding='utf-8') as f: | |
| f.write(self._cross_file_generator(needs_exe_wrapper=True)) | |
| self.init(testdir, extra_args=['--cross-file=' + str(p)]) | |
| out = self.run_target('test') | |
| self.assertRegex(out, r'Skipped:\s*1\s*\n') | |
| def test_needs_exe_wrapper_false(self): | |
| testdir = os.path.join(self.unit_test_dir, '70 cross test passed') | |
| with tempfile.TemporaryDirectory() as d: | |
| p = Path(d) / 'crossfile' | |
| with p.open('wt', encoding='utf-8') as f: | |
| f.write(self._cross_file_generator(needs_exe_wrapper=False)) | |
| self.init(testdir, extra_args=['--cross-file=' + str(p)]) | |
| out = self.run_target('test') | |
| self.assertNotRegex(out, r'Skipped:\s*1\n') | |
| def test_needs_exe_wrapper_true_wrapper(self): | |
| testdir = os.path.join(self.unit_test_dir, '70 cross test passed') | |
| with tempfile.TemporaryDirectory() as d: | |
| s = Path(d) / 'wrapper.py' | |
| with s.open('wt', encoding='utf-8') as f: | |
| f.write(self._stub_exe_wrapper()) | |
| s.chmod(0o774) | |
| p = Path(d) / 'crossfile' | |
| with p.open('wt', encoding='utf-8') as f: | |
| f.write(self._cross_file_generator( | |
| needs_exe_wrapper=True, | |
| exe_wrapper=[str(s)])) | |
| self.init(testdir, extra_args=['--cross-file=' + str(p), '-Dexpect=true']) | |
| out = self.run_target('test') | |
| self.assertRegex(out, r'Ok:\s*3\s*\n') | |
| def test_cross_exe_passed_no_wrapper(self): | |
| testdir = os.path.join(self.unit_test_dir, '70 cross test passed') | |
| with tempfile.TemporaryDirectory() as d: | |
| p = Path(d) / 'crossfile' | |
| with p.open('wt', encoding='utf-8') as f: | |
| f.write(self._cross_file_generator(needs_exe_wrapper=True)) | |
| self.init(testdir, extra_args=['--cross-file=' + str(p)]) | |
| self.build() | |
| out = self.run_target('test') | |
| self.assertRegex(out, r'Skipped:\s*1\s*\n') | |
| # The test uses mocking and thus requires that the current process is the | |
| # one to run the Meson steps. If we are using an external test executable | |
| # (most commonly in Debian autopkgtests) then the mocking won't work. | |
| @skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, cannot use mocking.') | |
| def test_cross_file_system_paths(self): | |
| if is_windows(): | |
| raise SkipTest('system crossfile paths not defined for Windows (yet)') | |
| testdir = os.path.join(self.common_test_dir, '1 trivial') | |
| cross_content = self._cross_file_generator() | |
| with tempfile.TemporaryDirectory() as d: | |
| dir_ = os.path.join(d, 'meson', 'cross') | |
| os.makedirs(dir_) | |
| with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False, encoding='utf-8') as f: | |
| f.write(cross_content) | |
| name = os.path.basename(f.name) | |
| with mock.patch.dict(os.environ, {'XDG_DATA_HOME': d}): | |
| self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True) | |
| self.wipe() | |
| with mock.patch.dict(os.environ, {'XDG_DATA_DIRS': d}): | |
| os.environ.pop('XDG_DATA_HOME', None) | |
| self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True) | |
| self.wipe() | |
| with tempfile.TemporaryDirectory() as d: | |
| dir_ = os.path.join(d, '.local', 'share', 'meson', 'cross') | |
| os.makedirs(dir_) | |
| with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False, encoding='utf-8') as f: | |
| f.write(cross_content) | |
| name = os.path.basename(f.name) | |
| # If XDG_DATA_HOME is set in the environment running the | |
| # tests this test will fail, os mock the environment, pop | |
| # it, then test | |
| with mock.patch.dict(os.environ): | |
| os.environ.pop('XDG_DATA_HOME', None) | |
| with mock.patch('mesonbuild.coredata.os.path.expanduser', lambda x: x.replace('~', d)): | |
| self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True) | |
| self.wipe() | |
| def helper_create_cross_file(self, values): | |
| """Create a config file as a temporary file. | |
| values should be a nested dictionary structure of {section: {key: | |
| value}} | |
| """ | |
| filename = os.path.join(self.builddir, f'generated{self.current_config}.config') | |
| self.current_config += 1 | |
| with open(filename, 'wt', encoding='utf-8') as f: | |
| for section, entries in values.items(): | |
| f.write(f'[{section}]\n') | |
| for k, v in entries.items(): | |
| f.write(f"{k}={v!r}\n") | |
| return filename | |
| def test_cross_file_dirs(self): | |
| testcase = os.path.join(self.unit_test_dir, '59 native file override') | |
| self.init(testcase, default_args=False, | |
| extra_args=['--native-file', os.path.join(testcase, 'nativefile'), | |
| '--cross-file', os.path.join(testcase, 'crossfile'), | |
| '-Ddef_bindir=binbar', | |
| '-Ddef_datadir=databar', | |
| '-Ddef_includedir=includebar', | |
| '-Ddef_infodir=infobar', | |
| '-Ddef_libdir=libbar', | |
| '-Ddef_libexecdir=libexecbar', | |
| '-Ddef_localedir=localebar', | |
| '-Ddef_localstatedir=localstatebar', | |
| '-Ddef_mandir=manbar', | |
| '-Ddef_sbindir=sbinbar', | |
| '-Ddef_sharedstatedir=sharedstatebar', | |
| '-Ddef_sysconfdir=sysconfbar']) | |
| def test_cross_file_dirs_overridden(self): | |
| testcase = os.path.join(self.unit_test_dir, '59 native file override') | |
| self.init(testcase, default_args=False, | |
| extra_args=['--native-file', os.path.join(testcase, 'nativefile'), | |
| '--cross-file', os.path.join(testcase, 'crossfile'), | |
| '-Ddef_libdir=liblib', '-Dlibdir=liblib', | |
| '-Ddef_bindir=binbar', | |
| '-Ddef_datadir=databar', | |
| '-Ddef_includedir=includebar', | |
| '-Ddef_infodir=infobar', | |
| '-Ddef_libexecdir=libexecbar', | |
| '-Ddef_localedir=localebar', | |
| '-Ddef_localstatedir=localstatebar', | |
| '-Ddef_mandir=manbar', | |
| '-Ddef_sbindir=sbinbar', | |
| '-Ddef_sharedstatedir=sharedstatebar', | |
| '-Ddef_sysconfdir=sysconfbar']) | |
| def test_cross_file_dirs_chain(self): | |
| # crossfile2 overrides crossfile overrides nativefile | |
| testcase = os.path.join(self.unit_test_dir, '59 native file override') | |
| self.init(testcase, default_args=False, | |
| extra_args=['--native-file', os.path.join(testcase, 'nativefile'), | |
| '--cross-file', os.path.join(testcase, 'crossfile'), | |
| '--cross-file', os.path.join(testcase, 'crossfile2'), | |
| '-Ddef_bindir=binbar2', | |
| '-Ddef_datadir=databar', | |
| '-Ddef_includedir=includebar', | |
| '-Ddef_infodir=infobar', | |
| '-Ddef_libdir=libbar', | |
| '-Ddef_libexecdir=libexecbar', | |
| '-Ddef_localedir=localebar', | |
| '-Ddef_localstatedir=localstatebar', | |
| '-Ddef_mandir=manbar', | |
| '-Ddef_sbindir=sbinbar', | |
| '-Ddef_sharedstatedir=sharedstatebar', | |
| '-Ddef_sysconfdir=sysconfbar']) | |
| def test_user_options(self): | |
| # This is just a touch test for cross file, since the implementation | |
| # shares code after loading from the files | |
| testcase = os.path.join(self.common_test_dir, '40 options') | |
| config = self.helper_create_cross_file({'project options': {'testoption': 'some other value'}}) | |
| with self.assertRaises(subprocess.CalledProcessError) as cm: | |
| self.init(testcase, extra_args=['--cross-file', config]) | |
| self.assertRegex(cm.exception.stdout, r'Incorrect value to [a-z]+ option') | |
| def test_builtin_options(self): | |
| testcase = os.path.join(self.common_test_dir, '2 cpp') | |
| config = self.helper_create_cross_file({'built-in options': {'cpp_std': 'c++14'}}) | |
| self.init(testcase, extra_args=['--cross-file', config]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'cpp_std': | |
| self.assertEqual(each['value'], 'c++14') | |
| break | |
| else: | |
| self.fail('No c++ standard set?') | |
| def test_builtin_options_per_machine(self): | |
| """Test options that are allowed to be set on a per-machine basis. | |
| Such options could be passed twice, once for the build machine, and | |
| once for the host machine. I've picked pkg-config path, but any would | |
| do that can be set for both. | |
| """ | |
| testcase = os.path.join(self.common_test_dir, '2 cpp') | |
| cross = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/cross/path', 'cpp_std': 'c++17'}}) | |
| native = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/native/path', 'cpp_std': 'c++14'}}) | |
| # Ensure that PKG_CONFIG_PATH is not set in the environment | |
| with mock.patch.dict('os.environ'): | |
| for k in ['PKG_CONFIG_PATH', 'PKG_CONFIG_PATH_FOR_BUILD']: | |
| try: | |
| del os.environ[k] | |
| except KeyError: | |
| pass | |
| self.init(testcase, extra_args=['--cross-file', cross, '--native-file', native]) | |
| configuration = self.introspect('--buildoptions') | |
| found = 0 | |
| for each in configuration: | |
| if each['name'] == 'pkg_config_path': | |
| self.assertEqual(each['value'], ['/cross/path']) | |
| found += 1 | |
| elif each['name'] == 'cpp_std': | |
| self.assertEqual(each['value'], 'c++17') | |
| found += 1 | |
| elif each['name'] == 'build.pkg_config_path': | |
| self.assertEqual(each['value'], ['/native/path']) | |
| found += 1 | |
| elif each['name'] == 'build.cpp_std': | |
| self.assertEqual(each['value'], 'c++14') | |
| found += 1 | |
| if found == 4: | |
| break | |
| self.assertEqual(found, 4, 'Did not find all sections.') | |
| def test_builtin_options_conf_overrides_env(self): | |
| testcase = os.path.join(self.common_test_dir, '2 cpp') | |
| config = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/native', 'cpp_args': '-DFILE'}}) | |
| cross = self.helper_create_cross_file({'built-in options': {'pkg_config_path': '/cross', 'cpp_args': '-DFILE'}}) | |
| self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross], | |
| override_envvars={'PKG_CONFIG_PATH': '/bar', 'PKG_CONFIG_PATH_FOR_BUILD': '/dir', | |
| 'CXXFLAGS': '-DENV', 'CXXFLAGS_FOR_BUILD': '-DENV'}) | |
| configuration = self.introspect('--buildoptions') | |
| found = 0 | |
| expected = 4 | |
| for each in configuration: | |
| if each['name'] == 'pkg_config_path': | |
| self.assertEqual(each['value'], ['/cross']) | |
| found += 1 | |
| elif each['name'] == 'build.pkg_config_path': | |
| self.assertEqual(each['value'], ['/native']) | |
| found += 1 | |
| elif each['name'].endswith('cpp_args'): | |
| self.assertEqual(each['value'], ['-DFILE']) | |
| found += 1 | |
| if found == expected: | |
| break | |
| self.assertEqual(found, expected, 'Did not find all sections.') | |
| def test_for_build_env_vars(self) -> None: | |
| testcase = os.path.join(self.common_test_dir, '2 cpp') | |
| config = self.helper_create_cross_file({'built-in options': {}}) | |
| cross = self.helper_create_cross_file({'built-in options': {}}) | |
| self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross], | |
| override_envvars={'PKG_CONFIG_PATH': '/bar', 'PKG_CONFIG_PATH_FOR_BUILD': '/dir'}) | |
| configuration = self.introspect('--buildoptions') | |
| found = 0 | |
| for each in configuration: | |
| if each['name'] == 'pkg_config_path': | |
| self.assertEqual(each['value'], ['/bar']) | |
| found += 1 | |
| elif each['name'] == 'build.pkg_config_path': | |
| self.assertEqual(each['value'], ['/dir']) | |
| found += 1 | |
| if found == 2: | |
| break | |
| self.assertEqual(found, 2, 'Did not find all sections.') | |
| def test_project_options_native_only(self) -> None: | |
| # Do not load project options from a native file when doing a cross | |
| # build | |
| testcase = os.path.join(self.unit_test_dir, '19 array option') | |
| config = self.helper_create_cross_file({'project options': {'list': ['bar', 'foo']}}) | |
| cross = self.helper_create_cross_file({'binaries': {}}) | |
| self.init(testcase, extra_args=['--native-file', config, '--cross-file', cross]) | |
| configuration = self.introspect('--buildoptions') | |
| for each in configuration: | |
| if each['name'] == 'list': | |
| self.assertEqual(each['value'], ['foo', 'bar']) | |
| break | |
| else: | |
| self.fail('Did not find expected option.') |