| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2016-2021 The Meson development team | |
| import subprocess | |
| import re | |
| import os | |
| import shutil | |
| from unittest import mock, SkipTest, skipUnless, skipIf | |
| from glob import glob | |
| 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_cygwin, python_command, version_compare, | |
| EnvironmentException | |
| ) | |
| from mesonbuild.options import OptionKey | |
| from mesonbuild.compilers import ( | |
| detect_c_compiler, detect_d_compiler, compiler_from_language, | |
| ) | |
| from mesonbuild.programs import ExternalProgram | |
| import mesonbuild.dependencies.base | |
| import mesonbuild.modules.pkgconfig | |
| from run_tests import ( | |
| Backend, get_fake_env | |
| ) | |
| from .baseplatformtests import BasePlatformTests | |
| from .helpers import * | |
| @skipUnless(is_windows() or is_cygwin(), "requires Windows (or Windows via Cygwin)") | |
| class WindowsTests(BasePlatformTests): | |
| ''' | |
| Tests that should run on Cygwin, MinGW, and MSVC | |
| ''' | |
| def setUp(self): | |
| super().setUp() | |
| self.platform_test_dir = os.path.join(self.src_root, 'test cases/windows') | |
| @skipIf(is_cygwin(), 'Test only applicable to Windows') | |
| @mock.patch.dict(os.environ) | |
| def test_find_program(self): | |
| ''' | |
| Test that Windows-specific edge-cases in find_program are functioning | |
| correctly. Cannot be an ordinary test because it involves manipulating | |
| PATH to point to a directory with Python scripts. | |
| ''' | |
| testdir = os.path.join(self.platform_test_dir, '8 find program') | |
| # Find `cmd` and `cmd.exe` | |
| prog1 = ExternalProgram('cmd') | |
| self.assertTrue(prog1.found(), msg='cmd not found') | |
| prog2 = ExternalProgram('cmd.exe') | |
| self.assertTrue(prog2.found(), msg='cmd.exe not found') | |
| self.assertPathEqual(prog1.get_path(), prog2.get_path()) | |
| # Find cmd.exe with args without searching | |
| prog = ExternalProgram('cmd', command=['cmd', '/C']) | |
| self.assertTrue(prog.found(), msg='cmd not found with args') | |
| self.assertPathEqual(prog.get_command()[0], 'cmd') | |
| # Find cmd with an absolute path that's missing the extension | |
| cmd_path = prog2.get_path()[:-4] | |
| prog = ExternalProgram(cmd_path) | |
| self.assertTrue(prog.found(), msg=f'{cmd_path!r} not found') | |
| # Finding a script with no extension inside a directory works | |
| prog = ExternalProgram(os.path.join(testdir, 'test-script')) | |
| self.assertTrue(prog.found(), msg='test-script not found') | |
| # Finding a script with an extension inside a directory works | |
| prog = ExternalProgram(os.path.join(testdir, 'test-script-ext.py')) | |
| self.assertTrue(prog.found(), msg='test-script-ext.py not found') | |
| # Finding a script in PATH | |
| os.environ['PATH'] += os.pathsep + testdir | |
| # If `.PY` is in PATHEXT, scripts can be found as programs | |
| if '.PY' in [ext.upper() for ext in os.environ['PATHEXT'].split(';')]: | |
| # Finding a script in PATH w/o extension works and adds the interpreter | |
| prog = ExternalProgram('test-script-ext') | |
| self.assertTrue(prog.found(), msg='test-script-ext not found in PATH') | |
| self.assertPathEqual(prog.get_command()[0], python_command[0]) | |
| self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') | |
| # Finding a script in PATH with extension works and adds the interpreter | |
| prog = ExternalProgram('test-script-ext.py') | |
| self.assertTrue(prog.found(), msg='test-script-ext.py not found in PATH') | |
| self.assertPathEqual(prog.get_command()[0], python_command[0]) | |
| self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') | |
| # Using a script with an extension directly via command= works and adds the interpreter | |
| prog = ExternalProgram('test-script-ext.py', command=[os.path.join(testdir, 'test-script-ext.py'), '--help']) | |
| self.assertTrue(prog.found(), msg='test-script-ext.py with full path not picked up via command=') | |
| self.assertPathEqual(prog.get_command()[0], python_command[0]) | |
| self.assertPathEqual(prog.get_command()[2], '--help') | |
| self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py') | |
| # Using a script without an extension directly via command= works and adds the interpreter | |
| prog = ExternalProgram('test-script', command=[os.path.join(testdir, 'test-script'), '--help']) | |
| self.assertTrue(prog.found(), msg='test-script with full path not picked up via command=') | |
| self.assertPathEqual(prog.get_command()[0], python_command[0]) | |
| self.assertPathEqual(prog.get_command()[2], '--help') | |
| self.assertPathBasenameEqual(prog.get_path(), 'test-script') | |
| # Ensure that WindowsApps gets removed from PATH | |
| path = os.environ['PATH'] | |
| if 'WindowsApps' not in path: | |
| username = os.environ['USERNAME'] | |
| appstore_dir = fr'C:\Users\{username}\AppData\Local\Microsoft\WindowsApps' | |
| path = os.pathsep + appstore_dir | |
| path = ExternalProgram._windows_sanitize_path(path) | |
| self.assertNotIn('WindowsApps', path) | |
| def test_ignore_libs(self): | |
| ''' | |
| Test that find_library on libs that are to be ignored returns an empty | |
| array of arguments. Must be a unit test because we cannot inspect | |
| ExternalLibraryHolder from build files. | |
| ''' | |
| testdir = os.path.join(self.platform_test_dir, '1 basic') | |
| env = get_fake_env(testdir, self.builddir, self.prefix) | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| if cc.get_argument_syntax() != 'msvc': | |
| raise SkipTest('Not using MSVC') | |
| # To force people to update this test, and also test | |
| self.assertEqual(set(cc.ignore_libs), {'c', 'm', 'pthread', 'dl', 'rt', 'execinfo'}) | |
| for l in cc.ignore_libs: | |
| self.assertEqual(cc.find_library(l, env, []), []) | |
| def test_rc_depends_files(self): | |
| testdir = os.path.join(self.platform_test_dir, '5 resources') | |
| # resource compiler depfile generation is not yet implemented for msvc | |
| env = get_fake_env(testdir, self.builddir, self.prefix) | |
| depfile_works = detect_c_compiler(env, MachineChoice.HOST).get_id() not in {'msvc', 'clang-cl', 'intel-cl'} | |
| self.init(testdir) | |
| self.build() | |
| # Immediately rebuilding should not do anything | |
| self.assertBuildIsNoop() | |
| # Test compile_resources(depend_file:) | |
| # Changing mtime of sample.ico should rebuild prog | |
| self.utime(os.path.join(testdir, 'res', 'sample.ico')) | |
| self.assertRebuiltTarget('prog') | |
| # Test depfile generation by compile_resources | |
| # Changing mtime of resource.h should rebuild myres.rc and then prog | |
| if depfile_works: | |
| self.utime(os.path.join(testdir, 'inc', 'resource', 'resource.h')) | |
| self.assertRebuiltTarget('prog') | |
| self.wipe() | |
| if depfile_works: | |
| testdir = os.path.join(self.platform_test_dir, '12 resources with custom targets') | |
| self.init(testdir) | |
| self.build() | |
| # Immediately rebuilding should not do anything | |
| self.assertBuildIsNoop() | |
| # Changing mtime of resource.h should rebuild myres_1.rc and then prog_1 | |
| self.utime(os.path.join(testdir, 'res', 'resource.h')) | |
| self.assertRebuiltTarget('prog_1') | |
| def test_msvc_cpp17(self): | |
| testdir = os.path.join(self.unit_test_dir, '44 vscpp17') | |
| env = get_fake_env(testdir, self.builddir, self.prefix) | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| if cc.get_argument_syntax() != 'msvc': | |
| raise SkipTest('Test only applies to MSVC-like compilers') | |
| try: | |
| self.init(testdir) | |
| except subprocess.CalledProcessError: | |
| # According to Python docs, output is only stored when | |
| # using check_output. We don't use it, so we can't check | |
| # that the output is correct (i.e. that it failed due | |
| # to the right reason). | |
| return | |
| self.build() | |
| @skipIf(is_cygwin(), 'Test only applicable to Windows') | |
| def test_genvslite(self): | |
| # The test framework itself might be forcing a specific, non-ninja backend across a set of tests, which | |
| # includes this test. E.g. - | |
| # > python.exe run_unittests.py --backend=vs WindowsTests | |
| # Since that explicitly specifies a backend that's incompatible with (and essentially meaningless in | |
| # conjunction with) 'genvslite', we should skip further genvslite testing. | |
| if self.backend is not Backend.ninja: | |
| raise SkipTest('Test only applies when using the Ninja backend') | |
| testdir = os.path.join(self.unit_test_dir, '118 genvslite') | |
| env = get_fake_env(testdir, self.builddir, self.prefix) | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| if cc.get_argument_syntax() != 'msvc': | |
| raise SkipTest('Test only applies when MSVC tools are available.') | |
| # We want to run the genvslite setup. I.e. - | |
| # meson setup --genvslite vs2022 ... | |
| # which we should expect to generate the set of _debug/_debugoptimized/_release suffixed | |
| # build directories. Then we want to check that the solution/project build hooks (like clean, | |
| # build, and rebuild) end up ultimately invoking the 'meson compile ...' of the appropriately | |
| # suffixed build dir, for which we need to use 'msbuild.exe' | |
| # Find 'msbuild.exe' | |
| msbuildprog = ExternalProgram('msbuild.exe') | |
| self.assertTrue(msbuildprog.found(), msg='msbuild.exe not found') | |
| # Setup with '--genvslite ...' | |
| self.new_builddir() | |
| # Firstly, we'd like to check that meson errors if the user explicitly specifies a non-ninja backend | |
| # during setup. | |
| with self.assertRaises(subprocess.CalledProcessError) as cm: | |
| self.init(testdir, extra_args=['--genvslite', 'vs2022', '--backend', 'vs']) | |
| self.assertIn("specifying a non-ninja backend conflicts with a 'genvslite' setup", cm.exception.stdout) | |
| # Wrap the following bulk of setup and msbuild invocation testing in a try-finally because any exception, | |
| # failure, or success must always clean up any of the suffixed build dir folders that may have been generated. | |
| try: | |
| # Since this | |
| self.init(testdir, extra_args=['--genvslite', 'vs2022']) | |
| # We need to bear in mind that the BasePlatformTests framework creates and cleans up its own temporary | |
| # build directory. However, 'genvslite' creates a set of suffixed build directories which we'll have | |
| # to clean up ourselves. See 'finally' block below. | |
| # We intentionally skip the - | |
| # self.build() | |
| # step because we're wanting to test compilation/building through the solution/project's interface. | |
| # Execute the debug and release builds through the projects 'Build' hooks | |
| genvslite_vcxproj_path = str(os.path.join(self.builddir+'_vs', 'genvslite@exe.vcxproj')) | |
| # This use-case of invoking the .sln/.vcxproj build hooks, not through Visual Studio itself, but through | |
| # 'msbuild.exe', in a VS tools command prompt environment (e.g. "x64 Native Tools Command Prompt for VS 2022"), is a | |
| # problem: Such an environment sets the 'VSINSTALLDIR' variable which, mysteriously, has the side-effect of causing | |
| # the spawned 'meson compile' command to fail to find 'ninja' (and even when ninja can be found elsewhere, all the | |
| # compiler binaries that ninja wants to run also fail to be found). The PATH environment variable in the child python | |
| # (and ninja) processes are fundamentally stripped down of all the critical search paths required to run the ninja | |
| # compile work ... ONLY when 'VSINSTALLDIR' is set; without 'VSINSTALLDIR' set, the meson compile command does search | |
| # for and find ninja (ironically, it finds it under the path where VSINSTALLDIR pointed!). | |
| # For the above reason, this testing works around this bizarre behaviour by temporarily removing any 'VSINSTALLDIR' | |
| # variable, prior to invoking the builds - | |
| current_env = os.environ.copy() | |
| current_env.pop('VSINSTALLDIR', None) | |
| subprocess.check_call( | |
| ['msbuild', '-target:Build', '-property:Configuration=debug', genvslite_vcxproj_path], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| env=current_env) | |
| subprocess.check_call( | |
| ['msbuild', '-target:Build', '-property:Configuration=release', genvslite_vcxproj_path], | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| env=current_env) | |
| # Check this has actually built the appropriate exes | |
| exe_path = str(os.path.join(self.builddir+'_debug', 'genvslite.exe')) | |
| self.assertTrue(os.path.exists(exe_path)) | |
| rc = subprocess.run([exe_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| self.assertEqual(rc.returncode, 0, rc.stdout + rc.stderr) | |
| output_debug = rc.stdout | |
| self.assertEqual(output_debug, b'Debug\r\n' ) | |
| exe_path = str(os.path.join(self.builddir+'_release', 'genvslite.exe')) | |
| self.assertTrue(os.path.exists(exe_path)) | |
| output_release = subprocess.check_output([exe_path]) | |
| self.assertEqual( output_release, b'Non-debug\r\n' ) | |
| finally: | |
| # Clean up our special suffixed temporary build dirs | |
| suffixed_build_dirs = glob(self.builddir+'_*', recursive=False) | |
| for build_dir in suffixed_build_dirs: | |
| shutil.rmtree(build_dir) | |
| def test_install_pdb_introspection(self): | |
| testdir = os.path.join(self.platform_test_dir, '1 basic') | |
| env = get_fake_env(testdir, self.builddir, self.prefix) | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| if cc.get_argument_syntax() != 'msvc': | |
| raise SkipTest('Test only applies to MSVC-like compilers') | |
| self.init(testdir) | |
| installed = self.introspect('--installed') | |
| files = [os.path.basename(path) for path in installed.values()] | |
| self.assertIn('prog.pdb', files) | |
| def _check_ld(self, name: str, lang: str, expected: str) -> None: | |
| if not shutil.which(name): | |
| raise SkipTest(f'Could not find {name}.') | |
| envvars = [mesonbuild.envconfig.ENV_VAR_PROG_MAP[f'{lang}_ld']] | |
| # Also test a deprecated variable if there is one. | |
| if f'{lang}_ld' in mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP: | |
| envvars.append( | |
| mesonbuild.envconfig.DEPRECATED_ENV_PROG_MAP[f'{lang}_ld']) | |
| for envvar in envvars: | |
| with mock.patch.dict(os.environ, {envvar: name}): | |
| env = get_fake_env() | |
| try: | |
| comp = compiler_from_language(env, lang, MachineChoice.HOST) | |
| except EnvironmentException: | |
| raise SkipTest(f'Could not find a compiler for {lang}') | |
| self.assertEqual(comp.linker.id, expected) | |
| def test_link_environment_variable_lld_link(self): | |
| env = get_fake_env() | |
| comp = detect_c_compiler(env, MachineChoice.HOST) | |
| if comp.get_argument_syntax() == 'gcc': | |
| raise SkipTest('GCC cannot be used with link compatible linkers.') | |
| self._check_ld('lld-link', 'c', 'lld-link') | |
| def test_link_environment_variable_link(self): | |
| env = get_fake_env() | |
| comp = detect_c_compiler(env, MachineChoice.HOST) | |
| if comp.get_argument_syntax() == 'gcc': | |
| raise SkipTest('GCC cannot be used with link compatible linkers.') | |
| self._check_ld('link', 'c', 'link') | |
| def test_link_environment_variable_optlink(self): | |
| env = get_fake_env() | |
| comp = detect_c_compiler(env, MachineChoice.HOST) | |
| if comp.get_argument_syntax() == 'gcc': | |
| raise SkipTest('GCC cannot be used with link compatible linkers.') | |
| self._check_ld('optlink', 'c', 'optlink') | |
| @skip_if_not_language('rust') | |
| def test_link_environment_variable_rust(self): | |
| self._check_ld('link', 'rust', 'link') | |
| @skip_if_not_language('d') | |
| def test_link_environment_variable_d(self): | |
| env = get_fake_env() | |
| comp = detect_d_compiler(env, MachineChoice.HOST) | |
| if comp.id == 'dmd': | |
| raise SkipTest('meson cannot reliably make DMD use a different linker.') | |
| self._check_ld('lld-link', 'd', 'lld-link') | |
| def test_pefile_checksum(self): | |
| try: | |
| import pefile | |
| except ImportError: | |
| if is_ci(): | |
| raise | |
| raise SkipTest('pefile module not found') | |
| testdir = os.path.join(self.common_test_dir, '6 linkshared') | |
| self.init(testdir, extra_args=['--buildtype=release']) | |
| self.build() | |
| # Test that binaries have a non-zero checksum | |
| env = get_fake_env() | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| cc_id = cc.get_id() | |
| ld_id = cc.get_linker_id() | |
| dll = glob(os.path.join(self.builddir, '*mycpplib.dll'))[0] | |
| exe = os.path.join(self.builddir, 'cppprog.exe') | |
| for f in (dll, exe): | |
| pe = pefile.PE(f) | |
| msg = f'PE file: {f!r}, compiler: {cc_id!r}, linker: {ld_id!r}' | |
| if cc_id == 'clang-cl': | |
| # Latest clang-cl tested (7.0) does not write checksums out | |
| self.assertFalse(pe.verify_checksum(), msg=msg) | |
| else: | |
| # Verify that a valid checksum was written by all other compilers | |
| self.assertTrue(pe.verify_checksum(), msg=msg) | |
| def test_qt5dependency_vscrt(self): | |
| ''' | |
| Test that qt5 dependencies use the debug module suffix when b_vscrt is | |
| set to 'mdd' | |
| ''' | |
| # Verify that the `b_vscrt` option is available | |
| env = get_fake_env() | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| if OptionKey('b_vscrt') not in cc.base_options: | |
| raise SkipTest('Compiler does not support setting the VS CRT') | |
| # Verify that qmake is for Qt5 | |
| if not shutil.which('qmake-qt5'): | |
| if not shutil.which('qmake') and not is_ci(): | |
| raise SkipTest('QMake not found') | |
| output = subprocess.getoutput('qmake --version') | |
| if 'Qt version 5' not in output and not is_ci(): | |
| raise SkipTest('Qmake found, but it is not for Qt 5.') | |
| # Setup with /MDd | |
| testdir = os.path.join(self.framework_test_dir, '4 qt') | |
| self.init(testdir, extra_args=['-Db_vscrt=mdd']) | |
| # Verify that we're linking to the debug versions of Qt DLLs | |
| build_ninja = os.path.join(self.builddir, 'build.ninja') | |
| with open(build_ninja, encoding='utf-8') as f: | |
| contents = f.read() | |
| m = re.search('build qt5core.exe: cpp_LINKER.*Qt5Cored.lib', contents) | |
| self.assertIsNotNone(m, msg=contents) | |
| def test_compiler_checks_vscrt(self): | |
| ''' | |
| Test that the correct VS CRT is used when running compiler checks | |
| ''' | |
| # Verify that the `b_vscrt` option is available | |
| env = get_fake_env() | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| if OptionKey('b_vscrt') not in cc.base_options: | |
| raise SkipTest('Compiler does not support setting the VS CRT') | |
| def sanitycheck_vscrt(vscrt): | |
| checks = self.get_meson_log_sanitychecks() | |
| self.assertGreater(len(checks), 0) | |
| for check in checks: | |
| self.assertIn(vscrt, check) | |
| testdir = os.path.join(self.common_test_dir, '1 trivial') | |
| self.init(testdir) | |
| sanitycheck_vscrt('/MDd') | |
| self.new_builddir() | |
| self.init(testdir, extra_args=['-Dbuildtype=debugoptimized']) | |
| sanitycheck_vscrt('/MD') | |
| self.new_builddir() | |
| self.init(testdir, extra_args=['-Dbuildtype=release']) | |
| sanitycheck_vscrt('/MD') | |
| self.new_builddir() | |
| self.init(testdir, extra_args=['-Db_vscrt=md']) | |
| sanitycheck_vscrt('/MD') | |
| self.new_builddir() | |
| self.init(testdir, extra_args=['-Db_vscrt=mdd']) | |
| sanitycheck_vscrt('/MDd') | |
| self.new_builddir() | |
| self.init(testdir, extra_args=['-Db_vscrt=mt']) | |
| sanitycheck_vscrt('/MT') | |
| self.new_builddir() | |
| self.init(testdir, extra_args=['-Db_vscrt=mtd']) | |
| sanitycheck_vscrt('/MTd') | |
| def test_modules(self): | |
| if self.backend is not Backend.ninja: | |
| raise SkipTest(f'C++ modules only work with the Ninja backend (not {self.backend.name}).') | |
| if 'VSCMD_VER' not in os.environ: | |
| raise SkipTest('C++ modules is only supported with Visual Studio.') | |
| if version_compare(os.environ['VSCMD_VER'], '<16.10.0'): | |
| raise SkipTest('C++ modules are only supported with VS 2019 Preview or newer.') | |
| self.init(os.path.join(self.unit_test_dir, '85 cpp modules')) | |
| self.build() | |
| def test_non_utf8_fails(self): | |
| # FIXME: VS backend does not use flags from compiler.get_always_args() | |
| # and thus it's missing /utf-8 argument. Was that intentional? This needs | |
| # to be revisited. | |
| if self.backend is not Backend.ninja: | |
| raise SkipTest(f'This test only pass with ninja backend (not {self.backend.name}).') | |
| testdir = os.path.join(self.platform_test_dir, '18 msvc charset') | |
| env = get_fake_env(testdir, self.builddir, self.prefix) | |
| cc = detect_c_compiler(env, MachineChoice.HOST) | |
| if cc.get_argument_syntax() != 'msvc': | |
| raise SkipTest('Not using MSVC') | |
| self.init(testdir, extra_args=['-Dtest-failure=true']) | |
| self.assertRaises(subprocess.CalledProcessError, self.build) | |
| @unittest.skipIf(is_cygwin(), "Needs visual studio") | |
| def test_vsenv_option(self): | |
| if self.backend is not Backend.ninja: | |
| raise SkipTest('Only ninja backend is valid for test') | |
| env = os.environ.copy() | |
| env['MESON_FORCE_VSENV_FOR_UNITTEST'] = '1' | |
| # Remove ninja from PATH to ensure that the one provided by Visual | |
| # Studio is picked, as a regression test for | |
| # https://github.com/mesonbuild/meson/issues/9774 | |
| env['PATH'] = get_path_without_cmd('ninja', env['PATH']) | |
| testdir = os.path.join(self.common_test_dir, '1 trivial') | |
| out = self.init(testdir, extra_args=['--vsenv'], override_envvars=env) | |
| self.assertIn('Activating VS', out) | |
| self.assertRegex(out, 'Visual Studio environment is needed to run Ninja') | |
| # All these directly call ninja with the full path, so we need to patch | |
| # it out to use meson subcommands | |
| with mock.patch.object(self, 'build_command', self.meson_command + ['compile']): | |
| out = self.build(override_envvars=env) | |
| self.assertIn('Activating VS', out) | |
| with mock.patch.object(self, 'test_command', self.meson_command + ['test']): | |
| out = self.run_tests(override_envvars=env) | |
| self.assertIn('Activating VS', out) | |
| with mock.patch.object(self, 'install_command', self.meson_command + ['install']): | |
| out = self.install(override_envvars=env) | |
| self.assertIn('Activating VS', out) |