| # 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 |
| 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.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'], universal_newlines=True, stderr=subprocess.STDOUT) |
| return 'Free Software Foundation' in out |
| |
| 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' |
| self.helper_for_compiler('objc', cb) |
| |
| @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' |
| self.helper_for_compiler('objcpp', cb) |
| |
| @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 lets 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, '112 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 |
| self.assertEqual(found, 4, 'Did not find all three 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': 'sentinal'} |
| }) |
| |
| 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('sentinal', args, msg="Did not find machine file value") |
| cargs_start = args.index('--') |
| sent_arg = args.index('sentinal') |
| self.assertLess(cargs_start, sent_arg, msg='sentinal 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.') |