# Copyright 2016-2021 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. 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']: rc = subprocess.call(['pkg-config', '--cflags', f'python{v}'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 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, '111 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.')