From 18bce476913de4deec18fcd028cb59d378c43812 Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Tue, 7 Feb 2017 17:22:18 +0530 Subject: [PATCH] find_program: Correctly use scripts found in PATH We also need to check whether the program found in PATH can be executed directly by Windows or if we need to figure out what the interpreter is and add it to the list. Also add `msc` to the list of extensions that can be executed natively Includes a project test and a unit test for this and all expected behaviours on Windows. --- mesonbuild/dependencies.py | 50 ++++++++++++++----- run_tests.py | 10 ++-- run_unittests.py | 49 +++++++++++++++++- test cases/windows/9 find program/meson.build | 8 +++ .../windows/9 find program/test-script-ext.py | 3 ++ 5 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 test cases/windows/9 find program/test-script-ext.py 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')