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.
936 lines
41 KiB
936 lines
41 KiB
3 years ago
|
# 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.
|
||
|
|
||
|
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 (
|
||
|
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, '47 native file binary')
|
||
|
self.current_config = 0
|
||
|
self.current_wrapper = 0
|
||
|
|
||
|
def helper_create_native_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():
|
||
|
if isinstance(v, (bool, int, float)):
|
||
|
f.write(f"{k}={v}\n")
|
||
|
elif isinstance(v, list):
|
||
|
f.write("{}=[{}]\n".format(k, ', '.join([f"'{w}'" for w in v])))
|
||
|
else:
|
||
|
f.write(f"{k}='{v}'\n")
|
||
|
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 langaugages
|
||
|
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('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, '60 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, '60 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, '79 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?')
|
||
|
|
||
|
|
||
|
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, '71 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, '71 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, '71 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, '71 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, can not 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) 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) 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, '60 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, '60 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, '60 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.')
|