1# Copyright 2016-2021 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import stat
16import subprocess
17import re
18import tempfile
19import textwrap
20import os
21import shutil
22import hashlib
23from unittest import mock, skipUnless, SkipTest
24from glob import glob
25from pathlib import Path
26import typing as T
27
28import mesonbuild.mlog
29import mesonbuild.depfile
30import mesonbuild.dependencies.base
31import mesonbuild.dependencies.factory
32import mesonbuild.envconfig
33import mesonbuild.environment
34import mesonbuild.coredata
35import mesonbuild.modules.gnome
36from mesonbuild.mesonlib import (
37    MachineChoice, is_windows, is_osx, is_cygwin, is_openbsd, is_haiku,
38    is_sunos, windows_proof_rmtree, version_compare, is_linux,
39    OptionKey, EnvironmentException
40)
41from mesonbuild.compilers import (
42    detect_c_compiler, detect_cpp_compiler, compiler_from_language,
43    AppleClangCCompiler, AppleClangCPPCompiler, AppleClangObjCCompiler,
44    AppleClangObjCPPCompiler
45)
46from mesonbuild.dependencies import PkgConfigDependency
47import mesonbuild.modules.pkgconfig
48
49
50from run_tests import (
51    get_fake_env
52)
53
54from .baseplatformtests import BasePlatformTests
55from .helpers import *
56
57def _clang_at_least(compiler: 'Compiler', minver: str, apple_minver: T.Optional[str]) -> bool:
58    """
59    check that Clang compiler is at least a specified version, whether AppleClang or regular Clang
60
61    Parameters
62    ----------
63    compiler:
64        Meson compiler object
65    minver: str
66        Clang minimum version
67    apple_minver: str
68        AppleCLang minimum version
69
70    Returns
71    -------
72    at_least: bool
73        Clang is at least the specified version
74    """
75    if isinstance(compiler, (AppleClangCCompiler, AppleClangCPPCompiler)):
76        if apple_minver is None:
77            return False
78        return version_compare(compiler.version, apple_minver)
79    return version_compare(compiler.version, minver)
80
81@skipUnless(not is_windows(), "requires something Unix-like")
82class LinuxlikeTests(BasePlatformTests):
83    '''
84    Tests that should run on Linux, macOS, and *BSD
85    '''
86
87    def test_basic_soname(self):
88        '''
89        Test that the soname is set correctly for shared libraries. This can't
90        be an ordinary test case because we need to run `readelf` and actually
91        check the soname.
92        https://github.com/mesonbuild/meson/issues/785
93        '''
94        testdir = os.path.join(self.common_test_dir, '4 shared')
95        self.init(testdir)
96        self.build()
97        lib1 = os.path.join(self.builddir, 'libmylib.so')
98        soname = get_soname(lib1)
99        self.assertEqual(soname, 'libmylib.so')
100
101    def test_custom_soname(self):
102        '''
103        Test that the soname is set correctly for shared libraries when
104        a custom prefix and/or suffix is used. This can't be an ordinary test
105        case because we need to run `readelf` and actually check the soname.
106        https://github.com/mesonbuild/meson/issues/785
107        '''
108        testdir = os.path.join(self.common_test_dir, '24 library versions')
109        self.init(testdir)
110        self.build()
111        lib1 = os.path.join(self.builddir, 'prefixsomelib.suffix')
112        soname = get_soname(lib1)
113        self.assertEqual(soname, 'prefixsomelib.suffix')
114
115    def test_pic(self):
116        '''
117        Test that -fPIC is correctly added to static libraries when b_staticpic
118        is true and not when it is false. This can't be an ordinary test case
119        because we need to inspect the compiler database.
120        '''
121        if is_windows() or is_cygwin() or is_osx():
122            raise SkipTest('PIC not relevant')
123
124        testdir = os.path.join(self.common_test_dir, '3 static')
125        self.init(testdir)
126        compdb = self.get_compdb()
127        self.assertIn('-fPIC', compdb[0]['command'])
128        self.setconf('-Db_staticpic=false')
129        # Regenerate build
130        self.build()
131        compdb = self.get_compdb()
132        self.assertNotIn('-fPIC', compdb[0]['command'])
133
134    @mock.patch.dict(os.environ)
135    def test_pkgconfig_gen(self):
136        '''
137        Test that generated pkg-config files can be found and have the correct
138        version and link args. This can't be an ordinary test case because we
139        need to run pkg-config outside of a Meson build file.
140        https://github.com/mesonbuild/meson/issues/889
141        '''
142        testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
143        self.init(testdir)
144        env = get_fake_env(testdir, self.builddir, self.prefix)
145        kwargs = {'required': True, 'silent': True}
146        os.environ['PKG_CONFIG_LIBDIR'] = self.privatedir
147        foo_dep = PkgConfigDependency('libfoo', env, kwargs)
148        self.assertTrue(foo_dep.found())
149        self.assertEqual(foo_dep.get_version(), '1.0')
150        self.assertIn('-lfoo', foo_dep.get_link_args())
151        self.assertEqual(foo_dep.get_pkgconfig_variable('foo', {}), 'bar')
152        self.assertPathEqual(foo_dep.get_pkgconfig_variable('datadir', {}), '/usr/data')
153
154        libhello_nolib = PkgConfigDependency('libhello_nolib', env, kwargs)
155        self.assertTrue(libhello_nolib.found())
156        self.assertEqual(libhello_nolib.get_link_args(), [])
157        self.assertEqual(libhello_nolib.get_compile_args(), [])
158        self.assertEqual(libhello_nolib.get_pkgconfig_variable('foo', {}), 'bar')
159        self.assertEqual(libhello_nolib.get_pkgconfig_variable('prefix', {}), self.prefix)
160        if version_compare(libhello_nolib.check_pkgconfig(libhello_nolib.pkgbin),">=0.29.1"):
161            self.assertEqual(libhello_nolib.get_pkgconfig_variable('escaped_var', {}), r'hello\ world')
162        self.assertEqual(libhello_nolib.get_pkgconfig_variable('unescaped_var', {}), 'hello world')
163
164        cc = detect_c_compiler(env, MachineChoice.HOST)
165        if cc.get_id() in {'gcc', 'clang'}:
166            for name in {'ct', 'ct0'}:
167                ct_dep = PkgConfigDependency(name, env, kwargs)
168                self.assertTrue(ct_dep.found())
169                self.assertIn('-lct', ct_dep.get_link_args())
170
171    def test_pkgconfig_gen_deps(self):
172        '''
173        Test that generated pkg-config files correctly handle dependencies
174        '''
175        testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
176        self.init(testdir)
177        privatedir1 = self.privatedir
178
179        self.new_builddir()
180        testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen', 'dependencies')
181        self.init(testdir, override_envvars={'PKG_CONFIG_LIBDIR': privatedir1})
182        privatedir2 = self.privatedir
183
184        env = {
185            'PKG_CONFIG_LIBDIR': os.pathsep.join([privatedir1, privatedir2]),
186            'PKG_CONFIG_SYSTEM_LIBRARY_PATH': '/usr/lib',
187        }
188        self._run(['pkg-config', 'dependency-test', '--validate'], override_envvars=env)
189
190        # pkg-config strips some duplicated flags so we have to parse the
191        # generated file ourself.
192        expected = {
193            'Requires': 'libexposed',
194            'Requires.private': 'libfoo >= 1.0',
195            'Libs': '-L${libdir} -llibmain -pthread -lcustom',
196            'Libs.private': '-lcustom2 -L${libdir} -llibinternal',
197            'Cflags': '-I${includedir} -pthread -DCUSTOM',
198        }
199        if is_osx() or is_haiku():
200            expected['Cflags'] = expected['Cflags'].replace('-pthread ', '')
201        with open(os.path.join(privatedir2, 'dependency-test.pc'), encoding='utf-8') as f:
202            matched_lines = 0
203            for line in f:
204                parts = line.split(':', 1)
205                if parts[0] in expected:
206                    key = parts[0]
207                    val = parts[1].strip()
208                    expected_val = expected[key]
209                    self.assertEqual(expected_val, val)
210                    matched_lines += 1
211            self.assertEqual(len(expected), matched_lines)
212
213        cmd = ['pkg-config', 'requires-test']
214        out = self._run(cmd + ['--print-requires'], override_envvars=env).strip().split('\n')
215        if not is_openbsd():
216            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello']))
217        else:
218            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello']))
219
220        cmd = ['pkg-config', 'requires-private-test']
221        out = self._run(cmd + ['--print-requires-private'], override_envvars=env).strip().split('\n')
222        if not is_openbsd():
223            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello']))
224        else:
225            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello']))
226
227        cmd = ['pkg-config', 'pub-lib-order']
228        out = self._run(cmd + ['--libs'], override_envvars=env).strip().split()
229        self.assertEqual(out, ['-llibmain2', '-llibinternal'])
230
231        # See common/44 pkgconfig-gen/meson.build for description of the case this test
232        with open(os.path.join(privatedir1, 'simple2.pc'), encoding='utf-8') as f:
233            content = f.read()
234            self.assertIn('Libs: -L${libdir} -lsimple2 -lsimple1', content)
235            self.assertIn('Libs.private: -lz', content)
236
237        with open(os.path.join(privatedir1, 'simple3.pc'), encoding='utf-8') as f:
238            content = f.read()
239            self.assertEqual(1, content.count('-lsimple3'))
240
241        with open(os.path.join(privatedir1, 'simple5.pc'), encoding='utf-8') as f:
242            content = f.read()
243            self.assertNotIn('-lstat2', content)
244
245    @mock.patch.dict(os.environ)
246    def test_pkgconfig_uninstalled(self):
247        testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
248        self.init(testdir)
249        self.build()
250
251        os.environ['PKG_CONFIG_LIBDIR'] = os.path.join(self.builddir, 'meson-uninstalled')
252        if is_cygwin():
253            os.environ['PATH'] += os.pathsep + self.builddir
254
255        self.new_builddir()
256        testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen', 'dependencies')
257        self.init(testdir)
258        self.build()
259        self.run_tests()
260
261    def test_pkg_unfound(self):
262        testdir = os.path.join(self.unit_test_dir, '23 unfound pkgconfig')
263        self.init(testdir)
264        with open(os.path.join(self.privatedir, 'somename.pc'), encoding='utf-8') as f:
265            pcfile = f.read()
266        self.assertFalse('blub_blob_blib' in pcfile)
267
268    def test_symlink_builddir(self) -> None:
269        '''
270        Test using a symlink as either the builddir for "setup" or
271        the argument for "-C".
272        '''
273        testdir = os.path.join(self.common_test_dir, '1 trivial')
274
275        symdir = f'{self.builddir}-symlink'
276        os.symlink(self.builddir, symdir)
277        self.addCleanup(os.unlink, symdir)
278        self.change_builddir(symdir)
279
280        self.init(testdir)
281        self.build()
282        self._run(self.mtest_command)
283
284    def test_vala_c_warnings(self):
285        '''
286        Test that no warnings are emitted for C code generated by Vala. This
287        can't be an ordinary test case because we need to inspect the compiler
288        database.
289        https://github.com/mesonbuild/meson/issues/864
290        '''
291        if not shutil.which('valac'):
292            raise SkipTest('valac not installed.')
293        testdir = os.path.join(self.vala_test_dir, '5 target glib')
294        self.init(testdir)
295        compdb = self.get_compdb()
296        vala_command = None
297        c_command = None
298        for each in compdb:
299            if each['file'].endswith('GLib.Thread.c'):
300                vala_command = each['command']
301            elif each['file'].endswith('GLib.Thread.vala'):
302                continue
303            elif each['file'].endswith('retcode.c'):
304                c_command = each['command']
305            else:
306                m = 'Unknown file {!r} in vala_c_warnings test'.format(each['file'])
307                raise AssertionError(m)
308        self.assertIsNotNone(vala_command)
309        self.assertIsNotNone(c_command)
310        # -w suppresses all warnings, should be there in Vala but not in C
311        self.assertIn(" -w ", vala_command)
312        self.assertNotIn(" -w ", c_command)
313        # -Wall enables all warnings, should be there in C but not in Vala
314        self.assertNotIn(" -Wall ", vala_command)
315        self.assertIn(" -Wall ", c_command)
316        # -Werror converts warnings to errors, should always be there since it's
317        # injected by an unrelated piece of code and the project has werror=true
318        self.assertIn(" -Werror ", vala_command)
319        self.assertIn(" -Werror ", c_command)
320
321    @skipIfNoPkgconfig
322    def test_qtdependency_pkgconfig_detection(self):
323        '''
324        Test that qt4 and qt5 detection with pkgconfig works.
325        '''
326        # Verify Qt4 or Qt5 can be found with pkg-config
327        qt4 = subprocess.call(['pkg-config', '--exists', 'QtCore'])
328        qt5 = subprocess.call(['pkg-config', '--exists', 'Qt5Core'])
329        testdir = os.path.join(self.framework_test_dir, '4 qt')
330        self.init(testdir, extra_args=['-Dmethod=pkg-config'])
331        # Confirm that the dependency was found with pkg-config
332        mesonlog = self.get_meson_log()
333        if qt4 == 0:
334            self.assertRegex('\n'.join(mesonlog),
335                             r'Run-time dependency qt4 \(modules: Core\) found: YES 4.* \(pkg-config\)')
336        if qt5 == 0:
337            self.assertRegex('\n'.join(mesonlog),
338                             r'Run-time dependency qt5 \(modules: Core\) found: YES 5.* \(pkg-config\)')
339
340    @skip_if_not_base_option('b_sanitize')
341    def test_generate_gir_with_address_sanitizer(self):
342        if is_cygwin():
343            raise SkipTest('asan not available on Cygwin')
344        if is_openbsd():
345            raise SkipTest('-fsanitize=address is not supported on OpenBSD')
346
347        testdir = os.path.join(self.framework_test_dir, '7 gnome')
348        self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false'])
349        self.build()
350
351    def test_qt5dependency_qmake_detection(self):
352        '''
353        Test that qt5 detection with qmake works. This can't be an ordinary
354        test case because it involves setting the environment.
355        '''
356        # Verify that qmake is for Qt5
357        if not shutil.which('qmake-qt5'):
358            if not shutil.which('qmake'):
359                raise SkipTest('QMake not found')
360            output = subprocess.getoutput('qmake --version')
361            if 'Qt version 5' not in output:
362                raise SkipTest('Qmake found, but it is not for Qt 5.')
363        # Disable pkg-config codepath and force searching with qmake/qmake-qt5
364        testdir = os.path.join(self.framework_test_dir, '4 qt')
365        self.init(testdir, extra_args=['-Dmethod=qmake'])
366        # Confirm that the dependency was found with qmake
367        mesonlog = self.get_meson_log()
368        self.assertRegex('\n'.join(mesonlog),
369                         r'Run-time dependency qt5 \(modules: Core\) found: YES .* \(qmake\)\n')
370
371    def test_qt6dependency_qmake_detection(self):
372        '''
373        Test that qt6 detection with qmake works. This can't be an ordinary
374        test case because it involves setting the environment.
375        '''
376        # Verify that qmake is for Qt5
377        if not shutil.which('qmake-qt6'):
378            if not shutil.which('qmake'):
379                raise SkipTest('QMake not found')
380            output = subprocess.getoutput('qmake --version')
381            if 'Qt version 6' not in output:
382                raise SkipTest('Qmake found, but it is not for Qt 6.')
383        # Disable pkg-config codepath and force searching with qmake/qmake-qt6
384        testdir = os.path.join(self.framework_test_dir, '4 qt')
385        self.init(testdir, extra_args=['-Dmethod=qmake'])
386        # Confirm that the dependency was found with qmake
387        mesonlog = self.get_meson_log()
388        self.assertRegex('\n'.join(mesonlog),
389                         r'Run-time dependency qt6 \(modules: Core\) found: YES .* \(qmake\)\n')
390
391    def glob_sofiles_without_privdir(self, g):
392        files = glob(g)
393        return [f for f in files if not f.endswith('.p')]
394
395    def _test_soname_impl(self, libpath, install):
396        if is_cygwin() or is_osx():
397            raise SkipTest('Test only applicable to ELF and linuxlike sonames')
398
399        testdir = os.path.join(self.unit_test_dir, '1 soname')
400        self.init(testdir)
401        self.build()
402        if install:
403            self.install()
404
405        # File without aliases set.
406        nover = os.path.join(libpath, 'libnover.so')
407        self.assertPathExists(nover)
408        self.assertFalse(os.path.islink(nover))
409        self.assertEqual(get_soname(nover), 'libnover.so')
410        self.assertEqual(len(self.glob_sofiles_without_privdir(nover[:-3] + '*')), 1)
411
412        # File with version set
413        verset = os.path.join(libpath, 'libverset.so')
414        self.assertPathExists(verset + '.4.5.6')
415        self.assertEqual(os.readlink(verset), 'libverset.so.4')
416        self.assertEqual(get_soname(verset), 'libverset.so.4')
417        self.assertEqual(len(self.glob_sofiles_without_privdir(verset[:-3] + '*')), 3)
418
419        # File with soversion set
420        soverset = os.path.join(libpath, 'libsoverset.so')
421        self.assertPathExists(soverset + '.1.2.3')
422        self.assertEqual(os.readlink(soverset), 'libsoverset.so.1.2.3')
423        self.assertEqual(get_soname(soverset), 'libsoverset.so.1.2.3')
424        self.assertEqual(len(self.glob_sofiles_without_privdir(soverset[:-3] + '*')), 2)
425
426        # File with version and soversion set to same values
427        settosame = os.path.join(libpath, 'libsettosame.so')
428        self.assertPathExists(settosame + '.7.8.9')
429        self.assertEqual(os.readlink(settosame), 'libsettosame.so.7.8.9')
430        self.assertEqual(get_soname(settosame), 'libsettosame.so.7.8.9')
431        self.assertEqual(len(self.glob_sofiles_without_privdir(settosame[:-3] + '*')), 2)
432
433        # File with version and soversion set to different values
434        bothset = os.path.join(libpath, 'libbothset.so')
435        self.assertPathExists(bothset + '.1.2.3')
436        self.assertEqual(os.readlink(bothset), 'libbothset.so.1.2.3')
437        self.assertEqual(os.readlink(bothset + '.1.2.3'), 'libbothset.so.4.5.6')
438        self.assertEqual(get_soname(bothset), 'libbothset.so.1.2.3')
439        self.assertEqual(len(self.glob_sofiles_without_privdir(bothset[:-3] + '*')), 3)
440
441        # A shared_module that is not linked to anything
442        module = os.path.join(libpath, 'libsome_module.so')
443        self.assertPathExists(module)
444        self.assertFalse(os.path.islink(module))
445        self.assertEqual(get_soname(module), None)
446
447        # A shared_module that is not linked to an executable with link_with:
448        module = os.path.join(libpath, 'liblinked_module1.so')
449        self.assertPathExists(module)
450        self.assertFalse(os.path.islink(module))
451        self.assertEqual(get_soname(module), 'liblinked_module1.so')
452
453        # A shared_module that is not linked to an executable with dependencies:
454        module = os.path.join(libpath, 'liblinked_module2.so')
455        self.assertPathExists(module)
456        self.assertFalse(os.path.islink(module))
457        self.assertEqual(get_soname(module), 'liblinked_module2.so')
458
459    def test_soname(self):
460        self._test_soname_impl(self.builddir, False)
461
462    def test_installed_soname(self):
463        libdir = self.installdir + os.path.join(self.prefix, self.libdir)
464        self._test_soname_impl(libdir, True)
465
466    def test_compiler_check_flags_order(self):
467        '''
468        Test that compiler check flags override all other flags. This can't be
469        an ordinary test case because it needs the environment to be set.
470        '''
471        testdir = os.path.join(self.common_test_dir, '36 has function')
472        env = get_fake_env(testdir, self.builddir, self.prefix)
473        cpp = detect_cpp_compiler(env, MachineChoice.HOST)
474        Oflag = '-O3'
475        OflagCPP = Oflag
476        if cpp.get_id() in ('clang', 'gcc'):
477            # prevent developers from adding "int main(int argc, char **argv)"
478            # to small Meson checks unless these parameters are actually used
479            OflagCPP += ' -Werror=unused-parameter'
480        env = {'CFLAGS': Oflag,
481               'CXXFLAGS': OflagCPP}
482        self.init(testdir, override_envvars=env)
483        cmds = self.get_meson_log_compiler_checks()
484        for cmd in cmds:
485            if cmd[0] == 'ccache':
486                cmd = cmd[1:]
487            # Verify that -I flags from the `args` kwarg are first
488            # This is set in the '36 has function' test case
489            self.assertEqual(cmd[1], '-I/tmp')
490            # Verify that -O3 set via the environment is overridden by -O0
491            Oargs = [arg for arg in cmd if arg.startswith('-O')]
492            self.assertEqual(Oargs, [Oflag, '-O0'])
493
494    def _test_stds_impl(self, testdir: str, compiler: 'Compiler') -> None:
495        has_cpp17 = (compiler.get_id() not in {'clang', 'gcc'} or
496                     compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=5.0.0', '>=9.1') or
497                     compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=5.0.0'))
498        has_cpp2a_c17 = (compiler.get_id() not in {'clang', 'gcc'} or
499                         compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=6.0.0', '>=10.0') or
500                         compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0'))
501        has_cpp20 = (compiler.get_id() not in {'clang', 'gcc'} or
502                     compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=10.0.0', None) or
503                     compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=10.0.0'))
504        has_c18 = (compiler.get_id() not in {'clang', 'gcc'} or
505                   compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=8.0.0', '>=11.0') or
506                   compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0'))
507        # Check that all the listed -std=xxx options for this compiler work just fine when used
508        # https://en.wikipedia.org/wiki/Xcode#Latest_versions
509        # https://www.gnu.org/software/gcc/projects/cxx-status.html
510        key = OptionKey('std', lang=compiler.language)
511        for v in compiler.get_options()[key].choices:
512            # we do it like this to handle gnu++17,c++17 and gnu17,c17 cleanly
513            # thus, C++ first
514            if '++17' in v and not has_cpp17:
515                continue
516            elif '++2a' in v and not has_cpp2a_c17:  # https://en.cppreference.com/w/cpp/compiler_support
517                continue
518            elif '++20' in v and not has_cpp20:
519                continue
520            # now C
521            elif '17' in v and not has_cpp2a_c17:
522                continue
523            elif '18' in v and not has_c18:
524                continue
525            self.init(testdir, extra_args=[f'-D{key!s}={v}'])
526            cmd = self.get_compdb()[0]['command']
527            # c++03 and gnu++03 are not understood by ICC, don't try to look for them
528            skiplist = frozenset([
529                ('intel', 'c++03'),
530                ('intel', 'gnu++03')])
531            if v != 'none' and not (compiler.get_id(), v) in skiplist:
532                cmd_std = f" -std={v} "
533                self.assertIn(cmd_std, cmd)
534            try:
535                self.build()
536            except Exception:
537                print(f'{key!s} was {v!r}')
538                raise
539            self.wipe()
540        # Check that an invalid std option in CFLAGS/CPPFLAGS fails
541        # Needed because by default ICC ignores invalid options
542        cmd_std = '-std=FAIL'
543        if compiler.language == 'c':
544            env_flag_name = 'CFLAGS'
545        elif compiler.language == 'cpp':
546            env_flag_name = 'CXXFLAGS'
547        else:
548            raise NotImplementedError(f'Language {compiler.language} not defined.')
549        env = {}
550        env[env_flag_name] = cmd_std
551        with self.assertRaises((subprocess.CalledProcessError, EnvironmentException),
552                               msg='C compiler should have failed with -std=FAIL'):
553            self.init(testdir, override_envvars = env)
554            # ICC won't fail in the above because additional flags are needed to
555            # make unknown -std=... options errors.
556            self.build()
557
558    def test_compiler_c_stds(self):
559        '''
560        Test that C stds specified for this compiler can all be used. Can't be
561        an ordinary test because it requires passing options to meson.
562        '''
563        testdir = os.path.join(self.common_test_dir, '1 trivial')
564        env = get_fake_env(testdir, self.builddir, self.prefix)
565        cc = detect_c_compiler(env, MachineChoice.HOST)
566        self._test_stds_impl(testdir, cc)
567
568    def test_compiler_cpp_stds(self):
569        '''
570        Test that C++ stds specified for this compiler can all be used. Can't
571        be an ordinary test because it requires passing options to meson.
572        '''
573        testdir = os.path.join(self.common_test_dir, '2 cpp')
574        env = get_fake_env(testdir, self.builddir, self.prefix)
575        cpp = detect_cpp_compiler(env, MachineChoice.HOST)
576        self._test_stds_impl(testdir, cpp)
577
578    def test_unity_subproj(self):
579        testdir = os.path.join(self.common_test_dir, '42 subproject')
580        self.init(testdir, extra_args='--unity=subprojects')
581        pdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/simpletest*.p'))
582        self.assertEqual(len(pdirs), 1)
583        self.assertPathExists(os.path.join(pdirs[0], 'simpletest-unity0.c'))
584        sdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/*sublib*.p'))
585        self.assertEqual(len(sdirs), 1)
586        self.assertPathExists(os.path.join(sdirs[0], 'sublib-unity0.c'))
587        self.assertPathDoesNotExist(os.path.join(self.builddir, 'user@exe/user-unity.c'))
588        self.build()
589
590    def test_installed_modes(self):
591        '''
592        Test that files installed by these tests have the correct permissions.
593        Can't be an ordinary test because our installed_files.txt is very basic.
594        '''
595        if is_cygwin():
596            self.new_builddir_in_tempdir()
597        # Test file modes
598        testdir = os.path.join(self.common_test_dir, '12 data')
599        self.init(testdir)
600        self.install()
601
602        f = os.path.join(self.installdir, 'etc', 'etcfile.dat')
603        found_mode = stat.filemode(os.stat(f).st_mode)
604        want_mode = 'rw------T'
605        self.assertEqual(want_mode, found_mode[1:])
606
607        f = os.path.join(self.installdir, 'usr', 'bin', 'runscript.sh')
608        statf = os.stat(f)
609        found_mode = stat.filemode(statf.st_mode)
610        want_mode = 'rwxr-sr-x'
611        self.assertEqual(want_mode, found_mode[1:])
612        if os.getuid() == 0:
613            # The chown failed nonfatally if we're not root
614            self.assertEqual(0, statf.st_uid)
615            self.assertEqual(0, statf.st_gid)
616
617        f = os.path.join(self.installdir, 'usr', 'share', 'progname',
618                         'fileobject_datafile.dat')
619        orig = os.path.join(testdir, 'fileobject_datafile.dat')
620        statf = os.stat(f)
621        statorig = os.stat(orig)
622        found_mode = stat.filemode(statf.st_mode)
623        orig_mode = stat.filemode(statorig.st_mode)
624        self.assertEqual(orig_mode[1:], found_mode[1:])
625        self.assertEqual(os.getuid(), statf.st_uid)
626        if os.getuid() == 0:
627            # The chown failed nonfatally if we're not root
628            self.assertEqual(0, statf.st_gid)
629
630        self.wipe()
631        # Test directory modes
632        testdir = os.path.join(self.common_test_dir, '59 install subdir')
633        self.init(testdir)
634        self.install()
635
636        f = os.path.join(self.installdir, 'usr', 'share', 'sub1', 'second.dat')
637        statf = os.stat(f)
638        found_mode = stat.filemode(statf.st_mode)
639        want_mode = 'rwxr-x--t'
640        self.assertEqual(want_mode, found_mode[1:])
641        if os.getuid() == 0:
642            # The chown failed nonfatally if we're not root
643            self.assertEqual(0, statf.st_uid)
644
645    def test_installed_modes_extended(self):
646        '''
647        Test that files are installed with correct permissions using install_mode.
648        '''
649        if is_cygwin():
650            self.new_builddir_in_tempdir()
651        testdir = os.path.join(self.common_test_dir, '190 install_mode')
652        self.init(testdir)
653        self.build()
654        self.install()
655
656        for fsobj, want_mode in [
657                ('bin', 'drwxr-x---'),
658                ('bin/runscript.sh', '-rwxr-sr-x'),
659                ('bin/trivialprog', '-rwxr-sr-x'),
660                ('include', 'drwxr-x---'),
661                ('include/config.h', '-rw-rwSr--'),
662                ('include/rootdir.h', '-r--r--r-T'),
663                ('lib', 'drwxr-x---'),
664                ('lib/libstat.a', '-rw---Sr--'),
665                ('share', 'drwxr-x---'),
666                ('share/man', 'drwxr-x---'),
667                ('share/man/man1', 'drwxr-x---'),
668                ('share/man/man1/foo.1', '-r--r--r-T'),
669                ('share/sub1', 'drwxr-x---'),
670                ('share/sub1/second.dat', '-rwxr-x--t'),
671                ('subdir', 'drwxr-x---'),
672                ('subdir/data.dat', '-rw-rwSr--'),
673        ]:
674            f = os.path.join(self.installdir, 'usr', *fsobj.split('/'))
675            found_mode = stat.filemode(os.stat(f).st_mode)
676            self.assertEqual(want_mode, found_mode,
677                             msg=('Expected file %s to have mode %s but found %s instead.' %
678                                  (fsobj, want_mode, found_mode)))
679        # Ensure that introspect --installed works on all types of files
680        # FIXME: also verify the files list
681        self.introspect('--installed')
682
683    def test_install_umask(self):
684        '''
685        Test that files are installed with correct permissions using default
686        install umask of 022, regardless of the umask at time the worktree
687        was checked out or the build was executed.
688        '''
689        if is_cygwin():
690            self.new_builddir_in_tempdir()
691        # Copy source tree to a temporary directory and change permissions
692        # there to simulate a checkout with umask 002.
693        orig_testdir = os.path.join(self.unit_test_dir, '26 install umask')
694        # Create a new testdir under tmpdir.
695        tmpdir = os.path.realpath(tempfile.mkdtemp())
696        self.addCleanup(windows_proof_rmtree, tmpdir)
697        testdir = os.path.join(tmpdir, '26 install umask')
698        # Copy the tree using shutil.copyfile, which will use the current umask
699        # instead of preserving permissions of the old tree.
700        save_umask = os.umask(0o002)
701        self.addCleanup(os.umask, save_umask)
702        shutil.copytree(orig_testdir, testdir, copy_function=shutil.copyfile)
703        # Preserve the executable status of subdir/sayhello though.
704        os.chmod(os.path.join(testdir, 'subdir', 'sayhello'), 0o775)
705        self.init(testdir)
706        # Run the build under a 027 umask now.
707        os.umask(0o027)
708        self.build()
709        # And keep umask 027 for the install step too.
710        self.install()
711
712        for executable in [
713                'bin/prog',
714                'share/subdir/sayhello',
715        ]:
716            f = os.path.join(self.installdir, 'usr', *executable.split('/'))
717            found_mode = stat.filemode(os.stat(f).st_mode)
718            want_mode = '-rwxr-xr-x'
719            self.assertEqual(want_mode, found_mode,
720                             msg=('Expected file %s to have mode %s but found %s instead.' %
721                                  (executable, want_mode, found_mode)))
722
723        for directory in [
724                'usr',
725                'usr/bin',
726                'usr/include',
727                'usr/share',
728                'usr/share/man',
729                'usr/share/man/man1',
730                'usr/share/subdir',
731        ]:
732            f = os.path.join(self.installdir, *directory.split('/'))
733            found_mode = stat.filemode(os.stat(f).st_mode)
734            want_mode = 'drwxr-xr-x'
735            self.assertEqual(want_mode, found_mode,
736                             msg=('Expected directory %s to have mode %s but found %s instead.' %
737                                  (directory, want_mode, found_mode)))
738
739        for datafile in [
740                'include/sample.h',
741                'share/datafile.cat',
742                'share/file.dat',
743                'share/man/man1/prog.1',
744                'share/subdir/datafile.dog',
745        ]:
746            f = os.path.join(self.installdir, 'usr', *datafile.split('/'))
747            found_mode = stat.filemode(os.stat(f).st_mode)
748            want_mode = '-rw-r--r--'
749            self.assertEqual(want_mode, found_mode,
750                             msg=('Expected file %s to have mode %s but found %s instead.' %
751                                  (datafile, want_mode, found_mode)))
752
753    def test_cpp_std_override(self):
754        testdir = os.path.join(self.unit_test_dir, '6 std override')
755        self.init(testdir)
756        compdb = self.get_compdb()
757        # Don't try to use -std=c++03 as a check for the
758        # presence of a compiler flag, as ICC does not
759        # support it.
760        for i in compdb:
761            if 'prog98' in i['file']:
762                c98_comp = i['command']
763            if 'prog11' in i['file']:
764                c11_comp = i['command']
765            if 'progp' in i['file']:
766                plain_comp = i['command']
767        self.assertNotEqual(len(plain_comp), 0)
768        self.assertIn('-std=c++98', c98_comp)
769        self.assertNotIn('-std=c++11', c98_comp)
770        self.assertIn('-std=c++11', c11_comp)
771        self.assertNotIn('-std=c++98', c11_comp)
772        self.assertNotIn('-std=c++98', plain_comp)
773        self.assertNotIn('-std=c++11', plain_comp)
774        # Now werror
775        self.assertIn('-Werror', plain_comp)
776        self.assertNotIn('-Werror', c98_comp)
777
778    def test_run_installed(self):
779        if is_cygwin() or is_osx():
780            raise SkipTest('LD_LIBRARY_PATH and RPATH not applicable')
781
782        testdir = os.path.join(self.unit_test_dir, '7 run installed')
783        self.init(testdir)
784        self.build()
785        self.install()
786        installed_exe = os.path.join(self.installdir, 'usr/bin/prog')
787        installed_libdir = os.path.join(self.installdir, 'usr/foo')
788        installed_lib = os.path.join(installed_libdir, 'libfoo.so')
789        self.assertTrue(os.path.isfile(installed_exe))
790        self.assertTrue(os.path.isdir(installed_libdir))
791        self.assertTrue(os.path.isfile(installed_lib))
792        # Must fail when run without LD_LIBRARY_PATH to ensure that
793        # rpath has been properly stripped rather than pointing to the builddir.
794        self.assertNotEqual(subprocess.call(installed_exe, stderr=subprocess.DEVNULL), 0)
795        # When LD_LIBRARY_PATH is set it should start working.
796        # For some reason setting LD_LIBRARY_PATH in os.environ fails
797        # when all tests are run (but works when only this test is run),
798        # but doing this explicitly works.
799        env = os.environ.copy()
800        env['LD_LIBRARY_PATH'] = ':'.join([installed_libdir, env.get('LD_LIBRARY_PATH', '')])
801        self.assertEqual(subprocess.call(installed_exe, env=env), 0)
802        # Ensure that introspect --installed works
803        installed = self.introspect('--installed')
804        for v in installed.values():
805            self.assertTrue('prog' in v or 'foo' in v)
806
807    @skipIfNoPkgconfig
808    def test_order_of_l_arguments(self):
809        testdir = os.path.join(self.unit_test_dir, '8 -L -l order')
810        self.init(testdir, override_envvars={'PKG_CONFIG_PATH': testdir})
811        # NOTE: .pc file has -Lfoo -lfoo -Lbar -lbar but pkg-config reorders
812        # the flags before returning them to -Lfoo -Lbar -lfoo -lbar
813        # but pkgconf seems to not do that. Sigh. Support both.
814        expected_order = [('-L/me/first', '-lfoo1'),
815                          ('-L/me/second', '-lfoo2'),
816                          ('-L/me/first', '-L/me/second'),
817                          ('-lfoo1', '-lfoo2'),
818                          ('-L/me/second', '-L/me/third'),
819                          ('-L/me/third', '-L/me/fourth',),
820                          ('-L/me/third', '-lfoo3'),
821                          ('-L/me/fourth', '-lfoo4'),
822                          ('-lfoo3', '-lfoo4'),
823                          ]
824        with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as ifile:
825            for line in ifile:
826                if expected_order[0][0] in line:
827                    for first, second in expected_order:
828                        self.assertLess(line.index(first), line.index(second))
829                    return
830        raise RuntimeError('Linker entries not found in the Ninja file.')
831
832    def test_introspect_dependencies(self):
833        '''
834        Tests that mesonintrospect --dependencies returns expected output.
835        '''
836        testdir = os.path.join(self.framework_test_dir, '7 gnome')
837        self.init(testdir)
838        glib_found = False
839        gobject_found = False
840        deps = self.introspect('--dependencies')
841        self.assertIsInstance(deps, list)
842        for dep in deps:
843            self.assertIsInstance(dep, dict)
844            self.assertIn('name', dep)
845            self.assertIn('compile_args', dep)
846            self.assertIn('link_args', dep)
847            if dep['name'] == 'glib-2.0':
848                glib_found = True
849            elif dep['name'] == 'gobject-2.0':
850                gobject_found = True
851        self.assertTrue(glib_found)
852        self.assertTrue(gobject_found)
853        if subprocess.call(['pkg-config', '--exists', 'glib-2.0 >= 2.56.2']) != 0:
854            raise SkipTest('glib >= 2.56.2 needed for the rest')
855        targets = self.introspect('--targets')
856        docbook_target = None
857        for t in targets:
858            if t['name'] == 'generated-gdbus-docbook':
859                docbook_target = t
860                break
861        self.assertIsInstance(docbook_target, dict)
862        self.assertEqual(os.path.basename(t['filename'][0]), 'generated-gdbus-doc-' + os.path.basename(t['target_sources'][0]['sources'][0]))
863
864    def test_introspect_installed(self):
865        testdir = os.path.join(self.linuxlike_test_dir, '7 library versions')
866        self.init(testdir)
867
868        install = self.introspect('--installed')
869        install = {os.path.basename(k): v for k, v in install.items()}
870        print(install)
871        if is_osx():
872            the_truth = {
873                'libmodule.dylib': '/usr/lib/libmodule.dylib',
874                'libnoversion.dylib': '/usr/lib/libnoversion.dylib',
875                'libonlysoversion.5.dylib': '/usr/lib/libonlysoversion.5.dylib',
876                'libonlysoversion.dylib': '/usr/lib/libonlysoversion.dylib',
877                'libonlyversion.1.dylib': '/usr/lib/libonlyversion.1.dylib',
878                'libonlyversion.dylib': '/usr/lib/libonlyversion.dylib',
879                'libsome.0.dylib': '/usr/lib/libsome.0.dylib',
880                'libsome.dylib': '/usr/lib/libsome.dylib',
881            }
882            the_truth_2 = {'/usr/lib/libsome.dylib',
883                           '/usr/lib/libsome.0.dylib',
884            }
885        else:
886            the_truth = {
887                'libmodule.so': '/usr/lib/libmodule.so',
888                'libnoversion.so': '/usr/lib/libnoversion.so',
889                'libonlysoversion.so': '/usr/lib/libonlysoversion.so',
890                'libonlysoversion.so.5': '/usr/lib/libonlysoversion.so.5',
891                'libonlyversion.so': '/usr/lib/libonlyversion.so',
892                'libonlyversion.so.1': '/usr/lib/libonlyversion.so.1',
893                'libonlyversion.so.1.4.5': '/usr/lib/libonlyversion.so.1.4.5',
894                'libsome.so': '/usr/lib/libsome.so',
895                'libsome.so.0': '/usr/lib/libsome.so.0',
896                'libsome.so.1.2.3': '/usr/lib/libsome.so.1.2.3',
897            }
898            the_truth_2 = {'/usr/lib/libsome.so',
899                           '/usr/lib/libsome.so.0',
900                           '/usr/lib/libsome.so.1.2.3'}
901        self.assertDictEqual(install, the_truth)
902
903        targets = self.introspect('--targets')
904        for t in targets:
905            if t['name'] != 'some':
906                continue
907            self.assertSetEqual(the_truth_2, set(t['install_filename']))
908
909    def test_build_rpath(self):
910        if is_cygwin():
911            raise SkipTest('Windows PE/COFF binaries do not use RPATH')
912        testdir = os.path.join(self.unit_test_dir, '10 build_rpath')
913        self.init(testdir)
914        self.build()
915        build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
916        self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar')
917        build_rpath = get_rpath(os.path.join(self.builddir, 'progcxx'))
918        self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar')
919        self.install()
920        install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/prog'))
921        self.assertEqual(install_rpath, '/baz')
922        install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/progcxx'))
923        self.assertEqual(install_rpath, 'baz')
924
925    @skipIfNoPkgconfig
926    def test_build_rpath_pkgconfig(self):
927        '''
928        Test that current build artefacts (libs) are found first on the rpath,
929        manually specified rpath comes second and additional rpath elements (from
930        pkg-config files) come last
931        '''
932        if is_cygwin():
933            raise SkipTest('Windows PE/COFF binaries do not use RPATH')
934        testdir = os.path.join(self.unit_test_dir, '90 pkgconfig build rpath order')
935        self.init(testdir, override_envvars={'PKG_CONFIG_PATH': testdir})
936        self.build()
937        build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
938        self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar:/foo/dummy')
939        build_rpath = get_rpath(os.path.join(self.builddir, 'progcxx'))
940        self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar:/foo/dummy')
941        self.install()
942        install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/prog'))
943        self.assertEqual(install_rpath, '/baz:/foo/dummy')
944        install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/progcxx'))
945        self.assertEqual(install_rpath, 'baz:/foo/dummy')
946
947    def test_global_rpath(self):
948        if is_cygwin():
949            raise SkipTest('Windows PE/COFF binaries do not use RPATH')
950        if is_osx():
951            raise SkipTest('Global RPATHs via LDFLAGS not yet supported on MacOS (does anybody need it?)')
952
953        testdir = os.path.join(self.unit_test_dir, '80 global-rpath')
954        oldinstalldir = self.installdir
955
956        # Build and install an external library without DESTDIR.
957        # The external library generates a .pc file without an rpath.
958        yonder_dir = os.path.join(testdir, 'yonder')
959        yonder_prefix = os.path.join(oldinstalldir, 'yonder')
960        yonder_libdir = os.path.join(yonder_prefix, self.libdir)
961        self.prefix = yonder_prefix
962        self.installdir = yonder_prefix
963        self.init(yonder_dir)
964        self.build()
965        self.install(use_destdir=False)
966
967        # Since rpath has multiple valid formats we need to
968        # test that they are all properly used.
969        rpath_formats = [
970            ('-Wl,-rpath=', False),
971            ('-Wl,-rpath,', False),
972            ('-Wl,--just-symbols=', True),
973            ('-Wl,--just-symbols,', True),
974            ('-Wl,-R', False),
975            ('-Wl,-R,', False)
976        ]
977        for rpath_format, exception in rpath_formats:
978            # Build an app that uses that installed library.
979            # Supply the rpath to the installed library via LDFLAGS
980            # (as systems like buildroot and guix are wont to do)
981            # and verify install preserves that rpath.
982            self.new_builddir()
983            env = {'LDFLAGS': rpath_format + yonder_libdir,
984                   'PKG_CONFIG_PATH': os.path.join(yonder_libdir, 'pkgconfig')}
985            if exception:
986                with self.assertRaises(subprocess.CalledProcessError):
987                    self.init(testdir, override_envvars=env)
988                continue
989            self.init(testdir, override_envvars=env)
990            self.build()
991            self.install(use_destdir=False)
992            got_rpath = get_rpath(os.path.join(yonder_prefix, 'bin/rpathified'))
993            self.assertEqual(got_rpath, yonder_libdir, rpath_format)
994
995    @skip_if_not_base_option('b_sanitize')
996    def test_pch_with_address_sanitizer(self):
997        if is_cygwin():
998            raise SkipTest('asan not available on Cygwin')
999        if is_openbsd():
1000            raise SkipTest('-fsanitize=address is not supported on OpenBSD')
1001
1002        testdir = os.path.join(self.common_test_dir, '13 pch')
1003        self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false'])
1004        self.build()
1005        compdb = self.get_compdb()
1006        for i in compdb:
1007            self.assertIn("-fsanitize=address", i["command"])
1008
1009    def test_cross_find_program(self):
1010        testdir = os.path.join(self.unit_test_dir, '11 cross prog')
1011        crossfile = tempfile.NamedTemporaryFile(mode='w')
1012        print(os.path.join(testdir, 'some_cross_tool.py'))
1013
1014        tool_path = os.path.join(testdir, 'some_cross_tool.py')
1015
1016        crossfile.write(textwrap.dedent(f'''\
1017            [binaries]
1018            c = '{shutil.which('gcc' if is_sunos() else 'cc')}'
1019            ar = '{shutil.which('ar')}'
1020            strip = '{shutil.which('strip')}'
1021            sometool.py = ['{tool_path}']
1022            someothertool.py = '{tool_path}'
1023
1024            [properties]
1025
1026            [host_machine]
1027            system = 'linux'
1028            cpu_family = 'arm'
1029            cpu = 'armv7' # Not sure if correct.
1030            endian = 'little'
1031            '''))
1032        crossfile.flush()
1033        self.meson_cross_file = crossfile.name
1034        self.init(testdir)
1035
1036    def test_reconfigure(self):
1037        testdir = os.path.join(self.unit_test_dir, '13 reconfigure')
1038        self.init(testdir, extra_args=['-Db_coverage=true'], default_args=False)
1039        self.build('reconfigure')
1040
1041    def test_vala_generated_source_buildir_inside_source_tree(self):
1042        '''
1043        Test that valac outputs generated C files in the expected location when
1044        the builddir is a subdir of the source tree.
1045        '''
1046        if not shutil.which('valac'):
1047            raise SkipTest('valac not installed.')
1048
1049        testdir = os.path.join(self.vala_test_dir, '8 generated sources')
1050        newdir = os.path.join(self.builddir, 'srctree')
1051        shutil.copytree(testdir, newdir)
1052        testdir = newdir
1053        # New builddir
1054        builddir = os.path.join(testdir, 'subdir/_build')
1055        os.makedirs(builddir, exist_ok=True)
1056        self.change_builddir(builddir)
1057        self.init(testdir)
1058        self.build()
1059
1060    def test_old_gnome_module_codepaths(self):
1061        '''
1062        A lot of code in the GNOME module is conditional on the version of the
1063        glib tools that are installed, and breakages in the old code can slip
1064        by once the CI has a newer glib version. So we force the GNOME module
1065        to pretend that it's running on an ancient glib so the fallback code is
1066        also tested.
1067        '''
1068        testdir = os.path.join(self.framework_test_dir, '7 gnome')
1069        mesonbuild.modules.gnome.native_glib_version = '2.20'
1070        env = {'MESON_UNIT_TEST_PRETEND_GLIB_OLD': "1"}
1071        try:
1072            self.init(testdir,
1073                      inprocess=True,
1074                      override_envvars=env)
1075            self.build(override_envvars=env)
1076        finally:
1077            mesonbuild.modules.gnome.native_glib_version = None
1078
1079    @skipIfNoPkgconfig
1080    def test_pkgconfig_usage(self):
1081        testdir1 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependency')
1082        testdir2 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependee')
1083        if subprocess.call(['pkg-config', '--cflags', 'glib-2.0'],
1084                           stdout=subprocess.DEVNULL,
1085                           stderr=subprocess.DEVNULL) != 0:
1086            raise SkipTest('Glib 2.0 dependency not available.')
1087        with tempfile.TemporaryDirectory() as tempdirname:
1088            self.init(testdir1, extra_args=['--prefix=' + tempdirname, '--libdir=lib'], default_args=False)
1089            self.install(use_destdir=False)
1090            shutil.rmtree(self.builddir)
1091            os.mkdir(self.builddir)
1092            pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig')
1093            self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'libpkgdep.pc')))
1094            lib_dir = os.path.join(tempdirname, 'lib')
1095            myenv = os.environ.copy()
1096            myenv['PKG_CONFIG_PATH'] = pkg_dir
1097            # Private internal libraries must not leak out.
1098            pkg_out = subprocess.check_output(['pkg-config', '--static', '--libs', 'libpkgdep'], env=myenv)
1099            self.assertFalse(b'libpkgdep-int' in pkg_out, 'Internal library leaked out.')
1100            # Dependencies must not leak to cflags when building only a shared library.
1101            pkg_out = subprocess.check_output(['pkg-config', '--cflags', 'libpkgdep'], env=myenv)
1102            self.assertFalse(b'glib' in pkg_out, 'Internal dependency leaked to headers.')
1103            # Test that the result is usable.
1104            self.init(testdir2, override_envvars=myenv)
1105            self.build(override_envvars=myenv)
1106            myenv = os.environ.copy()
1107            myenv['LD_LIBRARY_PATH'] = ':'.join([lib_dir, myenv.get('LD_LIBRARY_PATH', '')])
1108            if is_cygwin():
1109                bin_dir = os.path.join(tempdirname, 'bin')
1110                myenv['PATH'] = bin_dir + os.pathsep + myenv['PATH']
1111            self.assertTrue(os.path.isdir(lib_dir))
1112            test_exe = os.path.join(self.builddir, 'pkguser')
1113            self.assertTrue(os.path.isfile(test_exe))
1114            subprocess.check_call(test_exe, env=myenv)
1115
1116    @skipIfNoPkgconfig
1117    def test_pkgconfig_relative_paths(self):
1118        testdir = os.path.join(self.unit_test_dir, '62 pkgconfig relative paths')
1119        pkg_dir = os.path.join(testdir, 'pkgconfig')
1120        self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'librelativepath.pc')))
1121
1122        env = get_fake_env(testdir, self.builddir, self.prefix)
1123        env.coredata.set_options({OptionKey('pkg_config_path'): pkg_dir}, subproject='')
1124        kwargs = {'required': True, 'silent': True}
1125        relative_path_dep = PkgConfigDependency('librelativepath', env, kwargs)
1126        self.assertTrue(relative_path_dep.found())
1127
1128        # Ensure link_args are properly quoted
1129        libpath = Path(self.builddir) / '../relativepath/lib'
1130        link_args = ['-L' + libpath.as_posix(), '-lrelativepath']
1131        self.assertEqual(relative_path_dep.get_link_args(), link_args)
1132
1133    @skipIfNoPkgconfig
1134    def test_pkgconfig_duplicate_path_entries(self):
1135        testdir = os.path.join(self.unit_test_dir, '111 pkgconfig duplicate path entries')
1136        pkg_dir = os.path.join(testdir, 'pkgconfig')
1137
1138        env = get_fake_env(testdir, self.builddir, self.prefix)
1139        env.coredata.set_options({OptionKey('pkg_config_path'): pkg_dir}, subproject='')
1140
1141        PkgConfigDependency.setup_env({}, env, MachineChoice.HOST, pkg_dir)
1142        pkg_config_path = env.coredata.options[OptionKey('pkg_config_path')].value
1143        self.assertTrue(len(pkg_config_path) == 1)
1144
1145    @skipIfNoPkgconfig
1146    def test_pkgconfig_internal_libraries(self):
1147        '''
1148        '''
1149        with tempfile.TemporaryDirectory() as tempdirname:
1150            # build library
1151            testdirbase = os.path.join(self.unit_test_dir, '32 pkgconfig use libraries')
1152            testdirlib = os.path.join(testdirbase, 'lib')
1153            self.init(testdirlib, extra_args=['--prefix=' + tempdirname,
1154                                              '--libdir=lib',
1155                                              '--default-library=static'], default_args=False)
1156            self.build()
1157            self.install(use_destdir=False)
1158
1159            # build user of library
1160            pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig')
1161            self.new_builddir()
1162            self.init(os.path.join(testdirbase, 'app'),
1163                      override_envvars={'PKG_CONFIG_PATH': pkg_dir})
1164            self.build()
1165
1166    @skipIfNoPkgconfig
1167    def test_static_archive_stripping(self):
1168        '''
1169        Check that Meson produces valid static archives with --strip enabled
1170        '''
1171        with tempfile.TemporaryDirectory() as tempdirname:
1172            testdirbase = os.path.join(self.unit_test_dir, '66 static archive stripping')
1173
1174            # build lib
1175            self.new_builddir()
1176            testdirlib = os.path.join(testdirbase, 'lib')
1177            testlibprefix = os.path.join(tempdirname, 'libprefix')
1178            self.init(testdirlib, extra_args=['--prefix=' + testlibprefix,
1179                                              '--libdir=lib',
1180                                              '--default-library=static',
1181                                              '--buildtype=debug',
1182                                              '--strip'], default_args=False)
1183            self.build()
1184            self.install(use_destdir=False)
1185
1186            # build executable (uses lib, fails if static archive has been stripped incorrectly)
1187            pkg_dir = os.path.join(testlibprefix, 'lib/pkgconfig')
1188            self.new_builddir()
1189            self.init(os.path.join(testdirbase, 'app'),
1190                      override_envvars={'PKG_CONFIG_PATH': pkg_dir})
1191            self.build()
1192
1193    @skipIfNoPkgconfig
1194    def test_pkgconfig_formatting(self):
1195        testdir = os.path.join(self.unit_test_dir, '38 pkgconfig format')
1196        self.init(testdir)
1197        myenv = os.environ.copy()
1198        myenv['PKG_CONFIG_PATH'] = self.privatedir
1199        stdo = subprocess.check_output(['pkg-config', '--libs-only-l', 'libsomething'], env=myenv)
1200        deps = [b'-lgobject-2.0', b'-lgio-2.0', b'-lglib-2.0', b'-lsomething']
1201        if is_windows() or is_cygwin() or is_osx() or is_openbsd():
1202            # On Windows, libintl is a separate library
1203            deps.append(b'-lintl')
1204        self.assertEqual(set(deps), set(stdo.split()))
1205
1206    @skipIfNoPkgconfig
1207    @skip_if_not_language('cs')
1208    def test_pkgconfig_csharp_library(self):
1209        testdir = os.path.join(self.unit_test_dir, '50 pkgconfig csharp library')
1210        self.init(testdir)
1211        myenv = os.environ.copy()
1212        myenv['PKG_CONFIG_PATH'] = self.privatedir
1213        stdo = subprocess.check_output(['pkg-config', '--libs', 'libsomething'], env=myenv)
1214
1215        self.assertEqual("-r/usr/lib/libsomething.dll", str(stdo.decode('ascii')).strip())
1216
1217    @skipIfNoPkgconfig
1218    def test_pkgconfig_link_order(self):
1219        '''
1220        Test that libraries are listed before their dependencies.
1221        '''
1222        testdir = os.path.join(self.unit_test_dir, '53 pkgconfig static link order')
1223        self.init(testdir)
1224        myenv = os.environ.copy()
1225        myenv['PKG_CONFIG_PATH'] = self.privatedir
1226        stdo = subprocess.check_output(['pkg-config', '--libs', 'libsomething'], env=myenv)
1227        deps = stdo.split()
1228        self.assertTrue(deps.index(b'-lsomething') < deps.index(b'-ldependency'))
1229
1230    def test_deterministic_dep_order(self):
1231        '''
1232        Test that the dependencies are always listed in a deterministic order.
1233        '''
1234        testdir = os.path.join(self.unit_test_dir, '43 dep order')
1235        self.init(testdir)
1236        with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
1237            for line in bfile:
1238                if 'build myexe:' in line or 'build myexe.exe:' in line:
1239                    self.assertIn('liblib1.a liblib2.a', line)
1240                    return
1241        raise RuntimeError('Could not find the build rule')
1242
1243    def test_deterministic_rpath_order(self):
1244        '''
1245        Test that the rpaths are always listed in a deterministic order.
1246        '''
1247        if is_cygwin():
1248            raise SkipTest('rpath are not used on Cygwin')
1249        testdir = os.path.join(self.unit_test_dir, '42 rpath order')
1250        self.init(testdir)
1251        if is_osx():
1252            rpathre = re.compile(r'-rpath,.*/subprojects/sub1.*-rpath,.*/subprojects/sub2')
1253        else:
1254            rpathre = re.compile(r'-rpath,\$\$ORIGIN/subprojects/sub1:\$\$ORIGIN/subprojects/sub2')
1255        with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
1256            for line in bfile:
1257                if '-rpath' in line:
1258                    self.assertRegex(line, rpathre)
1259                    return
1260        raise RuntimeError('Could not find the rpath')
1261
1262    def test_override_with_exe_dep(self):
1263        '''
1264        Test that we produce the correct dependencies when a program is overridden with an executable.
1265        '''
1266        testdir = os.path.join(self.src_root, 'test cases', 'native', '9 override with exe')
1267        self.init(testdir)
1268        with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
1269            for line in bfile:
1270                if 'main1.c:' in line or 'main2.c:' in line:
1271                    self.assertIn('| subprojects/sub/foobar', line)
1272
1273    @skipIfNoPkgconfig
1274    def test_usage_external_library(self):
1275        '''
1276        Test that uninstalled usage of an external library (from the system or
1277        PkgConfigDependency) works. On macOS, this workflow works out of the
1278        box. On Linux, BSDs, Windows, etc, you need to set extra arguments such
1279        as LD_LIBRARY_PATH, etc, so this test is skipped.
1280
1281        The system library is found with cc.find_library() and pkg-config deps.
1282        '''
1283        oldprefix = self.prefix
1284        # Install external library so we can find it
1285        testdir = os.path.join(self.unit_test_dir, '40 external, internal library rpath', 'external library')
1286        # install into installdir without using DESTDIR
1287        installdir = self.installdir
1288        self.prefix = installdir
1289        self.init(testdir)
1290        self.prefix = oldprefix
1291        self.build()
1292        self.install(use_destdir=False)
1293        ## New builddir for the consumer
1294        self.new_builddir()
1295        env = {'LIBRARY_PATH': os.path.join(installdir, self.libdir),
1296               'PKG_CONFIG_PATH': os.path.join(installdir, self.libdir, 'pkgconfig')}
1297        testdir = os.path.join(self.unit_test_dir, '40 external, internal library rpath', 'built library')
1298        # install into installdir without using DESTDIR
1299        self.prefix = self.installdir
1300        self.init(testdir, override_envvars=env)
1301        self.prefix = oldprefix
1302        self.build(override_envvars=env)
1303        # test uninstalled
1304        self.run_tests(override_envvars=env)
1305        if not (is_osx() or is_linux()):
1306            return
1307        # test running after installation
1308        self.install(use_destdir=False)
1309        prog = os.path.join(self.installdir, 'bin', 'prog')
1310        self._run([prog])
1311        if not is_osx():
1312            # Rest of the workflow only works on macOS
1313            return
1314        out = self._run(['otool', '-L', prog])
1315        self.assertNotIn('@rpath', out)
1316        ## New builddir for testing that DESTDIR is not added to install_name
1317        self.new_builddir()
1318        # install into installdir with DESTDIR
1319        self.init(testdir, override_envvars=env)
1320        self.build(override_envvars=env)
1321        # test running after installation
1322        self.install(override_envvars=env)
1323        prog = self.installdir + os.path.join(self.prefix, 'bin', 'prog')
1324        lib = self.installdir + os.path.join(self.prefix, 'lib', 'libbar_built.dylib')
1325        for f in prog, lib:
1326            out = self._run(['otool', '-L', f])
1327            # Ensure that the otool output does not contain self.installdir
1328            self.assertNotRegex(out, self.installdir + '.*dylib ')
1329
1330    @skipIfNoPkgconfig
1331    def test_link_arg_fullname(self):
1332        '''
1333        Test for  support of -l:libfullname.a
1334        see: https://github.com/mesonbuild/meson/issues/9000
1335             https://stackoverflow.com/questions/48532868/gcc-library-option-with-a-colon-llibevent-a
1336        '''
1337        testdir = os.path.join(self.unit_test_dir, '97 link full name','libtestprovider')
1338        oldprefix = self.prefix
1339        # install into installdir without using DESTDIR
1340        installdir = self.installdir
1341        self.prefix = installdir
1342        self.init(testdir)
1343        self.prefix=oldprefix
1344        self.build()
1345        self.install(use_destdir=False)
1346
1347        self.new_builddir()
1348        env = {'LIBRARY_PATH': os.path.join(installdir, self.libdir),
1349               'PKG_CONFIG_PATH': os.path.join(installdir, self.libdir, 'pkgconfig')}
1350        testdir = os.path.join(self.unit_test_dir, '97 link full name','proguser')
1351        self.init(testdir,override_envvars=env)
1352
1353        # test for link with full path
1354        with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as bfile:
1355            for line in bfile:
1356                if 'build dprovidertest:' in line:
1357                    self.assertIn('/libtestprovider.a', line)
1358
1359        if is_osx():
1360            # macOS's ld do not supports `--whole-archive`, skip build & run
1361            return
1362
1363        self.build(override_envvars=env)
1364
1365        # skip test if pkg-config is too old.
1366        #   before v0.28, Libs flags like -Wl will not kept in context order with -l flags.
1367        #   see https://gitlab.freedesktop.org/pkg-config/pkg-config/-/blob/master/NEWS
1368        pkgconfigver = subprocess.check_output(['pkg-config', '--version'])
1369        if b'0.28' > pkgconfigver:
1370            raise SkipTest('pkg-config is too old to be correctly done this.')
1371        self.run_tests()
1372
1373    @skipIfNoPkgconfig
1374    def test_usage_pkgconfig_prefixes(self):
1375        '''
1376        Build and install two external libraries, to different prefixes,
1377        then build and install a client program that finds them via pkgconfig,
1378        and verify the installed client program runs.
1379        '''
1380        oldinstalldir = self.installdir
1381
1382        # Build and install both external libraries without DESTDIR
1383        val1dir = os.path.join(self.unit_test_dir, '75 pkgconfig prefixes', 'val1')
1384        val1prefix = os.path.join(oldinstalldir, 'val1')
1385        self.prefix = val1prefix
1386        self.installdir = val1prefix
1387        self.init(val1dir)
1388        self.build()
1389        self.install(use_destdir=False)
1390        self.new_builddir()
1391
1392        env1 = {}
1393        env1['PKG_CONFIG_PATH'] = os.path.join(val1prefix, self.libdir, 'pkgconfig')
1394        val2dir = os.path.join(self.unit_test_dir, '75 pkgconfig prefixes', 'val2')
1395        val2prefix = os.path.join(oldinstalldir, 'val2')
1396        self.prefix = val2prefix
1397        self.installdir = val2prefix
1398        self.init(val2dir, override_envvars=env1)
1399        self.build()
1400        self.install(use_destdir=False)
1401        self.new_builddir()
1402
1403        # Build, install, and run the client program
1404        env2 = {}
1405        env2['PKG_CONFIG_PATH'] = os.path.join(val2prefix, self.libdir, 'pkgconfig')
1406        testdir = os.path.join(self.unit_test_dir, '75 pkgconfig prefixes', 'client')
1407        testprefix = os.path.join(oldinstalldir, 'client')
1408        self.prefix = testprefix
1409        self.installdir = testprefix
1410        self.init(testdir, override_envvars=env2)
1411        self.build()
1412        self.install(use_destdir=False)
1413        prog = os.path.join(self.installdir, 'bin', 'client')
1414        env3 = {}
1415        if is_cygwin():
1416            env3['PATH'] = os.path.join(val1prefix, 'bin') + \
1417                os.pathsep + \
1418                os.path.join(val2prefix, 'bin') + \
1419                os.pathsep + os.environ['PATH']
1420        out = self._run([prog], override_envvars=env3).strip()
1421        # Expected output is val1 + val2 = 3
1422        self.assertEqual(out, '3')
1423
1424    def install_subdir_invalid_symlinks(self, testdir, subdir_path):
1425        '''
1426        Test that installation of broken symlinks works fine.
1427        https://github.com/mesonbuild/meson/issues/3914
1428        '''
1429        testdir = os.path.join(self.common_test_dir, testdir)
1430        subdir = os.path.join(testdir, subdir_path)
1431        with chdir(subdir):
1432            # Can't distribute broken symlinks in the source tree because it breaks
1433            # the creation of zipapps. Create it dynamically and run the test by
1434            # hand.
1435            src = '../../nonexistent.txt'
1436            os.symlink(src, 'invalid-symlink.txt')
1437            try:
1438                self.init(testdir)
1439                self.build()
1440                self.install()
1441                install_path = subdir_path.split(os.path.sep)[-1]
1442                link = os.path.join(self.installdir, 'usr', 'share', install_path, 'invalid-symlink.txt')
1443                self.assertTrue(os.path.islink(link), msg=link)
1444                self.assertEqual(src, os.readlink(link))
1445                self.assertFalse(os.path.isfile(link), msg=link)
1446            finally:
1447                os.remove(os.path.join(subdir, 'invalid-symlink.txt'))
1448
1449    def test_install_subdir_symlinks(self):
1450        self.install_subdir_invalid_symlinks('59 install subdir', os.path.join('sub', 'sub1'))
1451
1452    def test_install_subdir_symlinks_with_default_umask(self):
1453        self.install_subdir_invalid_symlinks('190 install_mode', 'sub2')
1454
1455    def test_install_subdir_symlinks_with_default_umask_and_mode(self):
1456        self.install_subdir_invalid_symlinks('190 install_mode', 'sub1')
1457
1458    @skipIfNoPkgconfigDep('gmodule-2.0')
1459    def test_ldflag_dedup(self):
1460        testdir = os.path.join(self.unit_test_dir, '52 ldflagdedup')
1461        if is_cygwin() or is_osx():
1462            raise SkipTest('Not applicable on Cygwin or OSX.')
1463        env = get_fake_env()
1464        cc = detect_c_compiler(env, MachineChoice.HOST)
1465        linker = cc.linker
1466        if not linker.export_dynamic_args(env):
1467            raise SkipTest('Not applicable for linkers without --export-dynamic')
1468        self.init(testdir)
1469        build_ninja = os.path.join(self.builddir, 'build.ninja')
1470        max_count = 0
1471        search_term = '-Wl,--export-dynamic'
1472        with open(build_ninja, encoding='utf-8') as f:
1473            for line in f:
1474                max_count = max(max_count, line.count(search_term))
1475        self.assertEqual(max_count, 1, 'Export dynamic incorrectly deduplicated.')
1476
1477    def test_compiler_libs_static_dedup(self):
1478        testdir = os.path.join(self.unit_test_dir, '56 dedup compiler libs')
1479        self.init(testdir)
1480        build_ninja = os.path.join(self.builddir, 'build.ninja')
1481        with open(build_ninja, encoding='utf-8') as f:
1482            lines = f.readlines()
1483        for lib in ('-ldl', '-lm', '-lc', '-lrt'):
1484            for line in lines:
1485                if lib not in line:
1486                    continue
1487                # Assert that
1488                self.assertEqual(len(line.split(lib)), 2, msg=(lib, line))
1489
1490    @skipIfNoPkgconfig
1491    def test_noncross_options(self):
1492        # C_std defined in project options must be in effect also when native compiling.
1493        testdir = os.path.join(self.unit_test_dir, '51 noncross options')
1494        self.init(testdir, extra_args=['-Dpkg_config_path=' + testdir])
1495        compdb = self.get_compdb()
1496        self.assertEqual(len(compdb), 2)
1497        self.assertRegex(compdb[0]['command'], '-std=c99')
1498        self.assertRegex(compdb[1]['command'], '-std=c99')
1499        self.build()
1500
1501    def test_identity_cross(self):
1502        testdir = os.path.join(self.unit_test_dir, '61 identity cross')
1503
1504        nativefile = tempfile.NamedTemporaryFile(mode='w')
1505        nativefile.write(textwrap.dedent('''\
1506            [binaries]
1507            c = ['{}']
1508            '''.format(os.path.join(testdir, 'build_wrapper.py'))))
1509        nativefile.flush()
1510        self.meson_native_file = nativefile.name
1511
1512        crossfile = tempfile.NamedTemporaryFile(mode='w')
1513        crossfile.write(textwrap.dedent('''\
1514            [binaries]
1515            c = ['{}']
1516            '''.format(os.path.join(testdir, 'host_wrapper.py'))))
1517        crossfile.flush()
1518        self.meson_cross_file = crossfile.name
1519
1520        # TODO should someday be explicit about build platform only here
1521        self.init(testdir)
1522
1523    def test_identity_cross_env(self):
1524        testdir = os.path.join(self.unit_test_dir, '61 identity cross')
1525        env = {
1526            'CC_FOR_BUILD': '"' + os.path.join(testdir, 'build_wrapper.py') + '"',
1527        }
1528        crossfile = tempfile.NamedTemporaryFile(mode='w')
1529        crossfile.write(textwrap.dedent('''\
1530            [binaries]
1531            c = ['{}']
1532            '''.format(os.path.join(testdir, 'host_wrapper.py'))))
1533        crossfile.flush()
1534        self.meson_cross_file = crossfile.name
1535        # TODO should someday be explicit about build platform only here
1536        self.init(testdir, override_envvars=env)
1537
1538    @skipIfNoPkgconfig
1539    def test_static_link(self):
1540        if is_cygwin():
1541            raise SkipTest("Cygwin doesn't support LD_LIBRARY_PATH.")
1542
1543        # Build some libraries and install them
1544        testdir = os.path.join(self.unit_test_dir, '67 static link/lib')
1545        libdir = os.path.join(self.installdir, self.libdir)
1546        oldprefix = self.prefix
1547        self.prefix = self.installdir
1548        self.init(testdir)
1549        self.install(use_destdir=False)
1550
1551        # Test that installed libraries works
1552        self.new_builddir()
1553        self.prefix = oldprefix
1554        meson_args = [f'-Dc_link_args=-L{libdir}',
1555                      '--fatal-meson-warnings']
1556        testdir = os.path.join(self.unit_test_dir, '67 static link')
1557        env = {'PKG_CONFIG_LIBDIR': os.path.join(libdir, 'pkgconfig')}
1558        self.init(testdir, extra_args=meson_args, override_envvars=env)
1559        self.build()
1560        self.run_tests()
1561
1562    def _check_ld(self, check: str, name: str, lang: str, expected: str) -> None:
1563        if is_sunos():
1564            raise SkipTest('Solaris currently cannot override the linker.')
1565        if not shutil.which(check):
1566            raise SkipTest(f'Could not find {check}.')
1567        envvars = [mesonbuild.envconfig.ENV_VAR_PROG_MAP[f'{lang}_ld']]
1568
1569        # Also test a deprecated variable if there is one.
1570        if f'{lang}_ld' in mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP:
1571            envvars.append(
1572                mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP[f'{lang}_ld'])
1573
1574        for envvar in envvars:
1575            with mock.patch.dict(os.environ, {envvar: name}):
1576                env = get_fake_env()
1577                comp = compiler_from_language(env, lang, MachineChoice.HOST)
1578                if isinstance(comp, (AppleClangCCompiler, AppleClangCPPCompiler,
1579                                     AppleClangObjCCompiler, AppleClangObjCPPCompiler)):
1580                    raise SkipTest('AppleClang is currently only supported with ld64')
1581                if lang != 'rust' and comp.use_linker_args('bfd') == []:
1582                    raise SkipTest(
1583                        f'Compiler {comp.id} does not support using alternative linkers')
1584                self.assertEqual(comp.linker.id, expected)
1585
1586    def test_ld_environment_variable_bfd(self):
1587        self._check_ld('ld.bfd', 'bfd', 'c', 'ld.bfd')
1588
1589    def test_ld_environment_variable_gold(self):
1590        self._check_ld('ld.gold', 'gold', 'c', 'ld.gold')
1591
1592    def test_ld_environment_variable_lld(self):
1593        self._check_ld('ld.lld', 'lld', 'c', 'ld.lld')
1594
1595    @skip_if_not_language('rust')
1596    @skipIfNoExecutable('ld.gold')  # need an additional check here because _check_ld checks for gcc
1597    def test_ld_environment_variable_rust(self):
1598        self._check_ld('gcc', 'gcc -fuse-ld=gold', 'rust', 'ld.gold')
1599
1600    def test_ld_environment_variable_cpp(self):
1601        self._check_ld('ld.gold', 'gold', 'cpp', 'ld.gold')
1602
1603    @skip_if_not_language('objc')
1604    def test_ld_environment_variable_objc(self):
1605        self._check_ld('ld.gold', 'gold', 'objc', 'ld.gold')
1606
1607    @skip_if_not_language('objcpp')
1608    def test_ld_environment_variable_objcpp(self):
1609        self._check_ld('ld.gold', 'gold', 'objcpp', 'ld.gold')
1610
1611    @skip_if_not_language('fortran')
1612    def test_ld_environment_variable_fortran(self):
1613        self._check_ld('ld.gold', 'gold', 'fortran', 'ld.gold')
1614
1615    @skip_if_not_language('d')
1616    def test_ld_environment_variable_d(self):
1617        # At least for me, ldc defaults to gold, and gdc defaults to bfd, so
1618        # let's pick lld, which isn't the default for either (currently)
1619        if is_osx():
1620            expected = 'ld64'
1621        else:
1622            expected = 'ld.lld'
1623        self._check_ld('ld.lld', 'lld', 'd', expected)
1624
1625    def compute_sha256(self, filename):
1626        with open(filename, 'rb') as f:
1627            return hashlib.sha256(f.read()).hexdigest()
1628
1629    def test_wrap_with_file_url(self):
1630        testdir = os.path.join(self.unit_test_dir, '73 wrap file url')
1631        source_filename = os.path.join(testdir, 'subprojects', 'foo.tar.xz')
1632        patch_filename = os.path.join(testdir, 'subprojects', 'foo-patch.tar.xz')
1633        wrap_filename = os.path.join(testdir, 'subprojects', 'foo.wrap')
1634        source_hash = self.compute_sha256(source_filename)
1635        patch_hash = self.compute_sha256(patch_filename)
1636        wrap = textwrap.dedent("""\
1637            [wrap-file]
1638            directory = foo
1639
1640            source_url = http://server.invalid/foo
1641            source_fallback_url = file://{}
1642            source_filename = foo.tar.xz
1643            source_hash = {}
1644
1645            patch_url = http://server.invalid/foo
1646            patch_fallback_url = file://{}
1647            patch_filename = foo-patch.tar.xz
1648            patch_hash = {}
1649            """.format(source_filename, source_hash, patch_filename, patch_hash))
1650        with open(wrap_filename, 'w', encoding='utf-8') as f:
1651            f.write(wrap)
1652        self.init(testdir)
1653        self.build()
1654        self.run_tests()
1655
1656        windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'packagecache'))
1657        windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'foo'))
1658        os.unlink(wrap_filename)
1659
1660    def test_no_rpath_for_static(self):
1661        testdir = os.path.join(self.common_test_dir, '5 linkstatic')
1662        self.init(testdir)
1663        self.build()
1664        build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
1665        self.assertIsNone(build_rpath)
1666
1667    def test_lookup_system_after_broken_fallback(self):
1668        # Just to generate libfoo.pc so we can test system dependency lookup.
1669        testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
1670        self.init(testdir)
1671        privatedir = self.privatedir
1672
1673        # Write test project where the first dependency() returns not-found
1674        # because 'broken' subproject does not exit, but that should not prevent
1675        # the 2nd dependency() to lookup on system.
1676        self.new_builddir()
1677        with tempfile.TemporaryDirectory() as d:
1678            with open(os.path.join(d, 'meson.build'), 'w', encoding='utf-8') as f:
1679                f.write(textwrap.dedent('''\
1680                    project('test')
1681                    dependency('notfound', fallback: 'broken', required: false)
1682                    dependency('libfoo', fallback: 'broken', required: true)
1683                    '''))
1684            self.init(d, override_envvars={'PKG_CONFIG_LIBDIR': privatedir})
1685
1686    def test_as_link_whole(self):
1687        testdir = os.path.join(self.unit_test_dir, '77 as link whole')
1688        self.init(testdir)
1689        with open(os.path.join(self.privatedir, 'bar1.pc'), encoding='utf-8') as f:
1690            content = f.read()
1691            self.assertIn('-lfoo', content)
1692        with open(os.path.join(self.privatedir, 'bar2.pc'), encoding='utf-8') as f:
1693            content = f.read()
1694            self.assertNotIn('-lfoo', content)
1695
1696    def test_prelinking(self):
1697        # Prelinking currently only works on recently new GNU toolchains.
1698        # Skip everything else. When support for other toolchains is added,
1699        # remove limitations as necessary.
1700        if is_osx():
1701            raise SkipTest('Prelinking not supported on Darwin.')
1702        if 'clang' in os.environ.get('CC', 'dummy'):
1703            raise SkipTest('Prelinking not supported with Clang.')
1704        testdir = os.path.join(self.unit_test_dir, '87 prelinking')
1705        env = get_fake_env(testdir, self.builddir, self.prefix)
1706        cc = detect_c_compiler(env, MachineChoice.HOST)
1707        if cc.id == "gcc":
1708            gccver = subprocess.check_output(['gcc', '--version'])
1709            if b'7.5.0' in gccver:
1710                raise SkipTest('GCC on Bionic is too old to be supported.')
1711        self.init(testdir)
1712        self.build()
1713        outlib = os.path.join(self.builddir, 'libprelinked.a')
1714        ar = shutil.which('ar')
1715        self.assertTrue(os.path.exists(outlib))
1716        self.assertTrue(ar is not None)
1717        p = subprocess.run([ar, 't', outlib],
1718                           stdout=subprocess.PIPE,
1719                           stderr=subprocess.DEVNULL,
1720                           universal_newlines=True, timeout=1)
1721        obj_files = p.stdout.strip().split('\n')
1722        self.assertEqual(len(obj_files), 1)
1723        self.assertTrue(obj_files[0].endswith('-prelink.o'))
1724
1725    def do_one_test_with_nativefile(self, testdir, args):
1726        testdir = os.path.join(self.common_test_dir, testdir)
1727        with tempfile.TemporaryDirectory() as d:
1728            p = Path(d) / 'nativefile'
1729            with p.open('wt', encoding='utf-8') as f:
1730                f.write(f'''[binaries]
1731                    c = {args}
1732                    ''')
1733            self.init(testdir, extra_args=['--native-file=' + str(p)])
1734            self.build()
1735
1736    def test_cmake_multilib(self):
1737        '''
1738        Test that the cmake module handles multilib paths correctly.
1739        '''
1740        # Verify that "gcc -m32" works
1741        try:
1742            self.do_one_test_with_nativefile('1 trivial', "['gcc', '-m32']")
1743        except subprocess.CalledProcessError as e:
1744            raise SkipTest('Not GCC, or GCC does not have the -m32 option')
1745        self.wipe()
1746
1747        # Verify that cmake works
1748        try:
1749            self.do_one_test_with_nativefile('../cmake/1 basic', "['gcc']")
1750        except subprocess.CalledProcessError as e:
1751            raise SkipTest('Could not build basic cmake project')
1752        self.wipe()
1753
1754        # If so, we can test that cmake works with "gcc -m32"
1755        self.do_one_test_with_nativefile('../cmake/1 basic', "['gcc', '-m32']")
1756