The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
970 lines
43 KiB
970 lines
43 KiB
# 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.')
|
|
|