diff --git a/mesonbuild/dependencies.py b/mesonbuild/dependencies.py index 9525ffa34..920a2798a 100644 --- a/mesonbuild/dependencies.py +++ b/mesonbuild/dependencies.py @@ -402,7 +402,7 @@ class WxDependency(Dependency): return self.is_found class ExternalProgram: - windows_exts = ('exe', 'com', 'bat') + windows_exts = ('exe', 'msc', 'com', 'bat') def __init__(self, name, fullpath=None, silent=False, search_dir=None): self.name = name @@ -420,6 +420,10 @@ class ExternalProgram: else: mlog.log('Program', mlog.bold(name), 'found:', mlog.red('NO')) + def __repr__(self): + r = '<{} {!r} -> {!r}>' + return r.format(self.__class__.__name__, self.name, self.fullpath) + @staticmethod def _shebang_to_cmd(script): """ @@ -473,27 +477,49 @@ class ExternalProgram: return self._shebang_to_cmd(trial) def _search(self, name, search_dir): + ''' + Search in the specified dir for the specified executable by name + and if not found search in PATH + ''' commands = self._search_dir(name, search_dir) if commands: return commands # Do a standard search in PATH fullpath = shutil.which(name) - if fullpath or not mesonlib.is_windows(): + if not mesonlib.is_windows(): # On UNIX-like platforms, the standard PATH search is enough return [fullpath] - # On Windows, if name is an absolute path, we need the extension too - for ext in self.windows_exts: - fullpath = '{}.{}'.format(name, ext) - if os.path.exists(fullpath): + # HERE BEGINS THE TERROR OF WINDOWS + if fullpath: + # On Windows, even if the PATH search returned a full path, we can't be + # sure that it can be run directly if it's not a native executable. + # For instance, interpreted scripts sometimes need to be run explicitly + # with an interpreter if the file association is not done properly. + name_ext = os.path.splitext(fullpath)[1] + if name_ext[1:].lower() in self.windows_exts: + # Good, it can be directly executed return [fullpath] - # On Windows, interpreted scripts must have an extension otherwise they - # cannot be found by a standard PATH search. So we do a custom search - # where we manually search for a script with a shebang in PATH. - search_dirs = os.environ.get('PATH', '').split(';') - for search_dir in search_dirs: - commands = self._search_dir(name, search_dir) + # Try to extract the interpreter from the shebang + commands = self._shebang_to_cmd(fullpath) if commands: return commands + else: + # Maybe the name is an absolute path to a native Windows + # executable, but without the extension. This is technically wrong, + # but many people do it because it works in the MinGW shell. + if os.path.isabs(name): + for ext in self.windows_exts: + fullpath = '{}.{}'.format(name, ext) + if os.path.exists(fullpath): + return [fullpath] + # On Windows, interpreted scripts must have an extension otherwise they + # cannot be found by a standard PATH search. So we do a custom search + # where we manually search for a script with a shebang in PATH. + search_dirs = os.environ.get('PATH', '').split(';') + for search_dir in search_dirs: + commands = self._search_dir(name, search_dir) + if commands: + return commands return [None] def found(self): diff --git a/run_tests.py b/run_tests.py index 62824400a..f2038e4cb 100755 --- a/run_tests.py +++ b/run_tests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2012-2016 The Meson development team +# Copyright 2012-2017 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,10 +21,12 @@ from mesonbuild import mesonlib if __name__ == '__main__': returncode = 0 print('Running unittests.\n') + units = ['InternalTests', 'AllPlatformTests'] if mesonlib.is_linux(): - returncode += subprocess.call([sys.executable, 'run_unittests.py', '-v']) - else: - returncode += subprocess.call([sys.executable, 'run_unittests.py', '-v', 'InternalTests', 'AllPlatformTests']) + units += ['LinuxlikeTests'] + elif mesonlib.is_windows(): + units += ['WindowsTests'] + returncode += subprocess.call([sys.executable, 'run_unittests.py', '-v'] + units) # Ubuntu packages do not have a binary without -6 suffix. if shutil.which('arm-linux-gnueabihf-gcc-6') and not platform.machine().startswith('arm'): print('Running cross compilation tests.\n') diff --git a/run_unittests.py b/run_unittests.py index 3facf4960..f72313a97 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2016 The Meson development team +# Copyright 2016-2017 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import mesonbuild.environment import mesonbuild.mesonlib from mesonbuild.mesonlib import is_windows from mesonbuild.environment import detect_ninja, Environment -from mesonbuild.dependencies import PkgConfigDependency +from mesonbuild.dependencies import PkgConfigDependency, ExternalProgram if is_windows(): exe_suffix = '.exe' @@ -186,6 +186,7 @@ class BasePlatformTests(unittest.TestCase): super().setUp() src_root = os.path.dirname(__file__) src_root = os.path.join(os.getcwd(), src_root) + self.src_root = src_root # In case the directory is inside a symlinked directory, find the real # path otherwise we might not find the srcdir from inside the builddir. self.builddir = os.path.realpath(tempfile.mkdtemp()) @@ -564,6 +565,50 @@ class AllPlatformTests(BasePlatformTests): self.assertPathBasenameEqual(incs[7], 'sub1') +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') + + 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, '9 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.fullpath[0], prog2.fullpath[0]) + # Find cmd with an absolute path that's missing the extension + cmd_path = prog2.fullpath[0][:-4] + prog = ExternalProgram(cmd_path) + self.assertTrue(prog.found(), msg='{!r} not found'.format(cmd_path)) + # 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 w/o extension works and adds the interpreter + os.environ['PATH'] += os.pathsep + testdir + prog = ExternalProgram('test-script-ext') + self.assertTrue(prog.found(), msg='test-script-ext not found in PATH') + self.assertPathEqual(prog.fullpath[0], sys.executable) + self.assertPathBasenameEqual(prog.fullpath[1], '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.fullpath[0], sys.executable) + self.assertPathBasenameEqual(prog.fullpath[1], 'test-script-ext.py') + + class LinuxlikeTests(BasePlatformTests): ''' Tests that should run on Linux and *BSD diff --git a/test cases/windows/9 find program/meson.build b/test cases/windows/9 find program/meson.build index ef3458685..565fb626d 100644 --- a/test cases/windows/9 find program/meson.build +++ b/test cases/windows/9 find program/meson.build @@ -1,4 +1,12 @@ project('find program', 'c') +# Test that we can find native windows executables +find_program('cmd') +find_program('cmd.exe') + +# Test that a script file with an extension can be found +ext = find_program('test-script-ext.py') +test('ext', ext) +# Test that a script file without an extension can be found prog = find_program('test-script') test('script', prog) diff --git a/test cases/windows/9 find program/test-script-ext.py b/test cases/windows/9 find program/test-script-ext.py new file mode 100644 index 000000000..ae9adfb8e --- /dev/null +++ b/test cases/windows/9 find program/test-script-ext.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +print('ext/noext')