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.
pull/1356/head
Nirbheek Chauhan 8 years ago
parent d5b4944924
commit 18bce47691
  1. 50
      mesonbuild/dependencies.py
  2. 10
      run_tests.py
  3. 49
      run_unittests.py
  4. 8
      test cases/windows/9 find program/meson.build
  5. 3
      test cases/windows/9 find program/test-script-ext.py

@ -402,7 +402,7 @@ class WxDependency(Dependency):
return self.is_found return self.is_found
class ExternalProgram: class ExternalProgram:
windows_exts = ('exe', 'com', 'bat') windows_exts = ('exe', 'msc', 'com', 'bat')
def __init__(self, name, fullpath=None, silent=False, search_dir=None): def __init__(self, name, fullpath=None, silent=False, search_dir=None):
self.name = name self.name = name
@ -420,6 +420,10 @@ class ExternalProgram:
else: else:
mlog.log('Program', mlog.bold(name), 'found:', mlog.red('NO')) 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 @staticmethod
def _shebang_to_cmd(script): def _shebang_to_cmd(script):
""" """
@ -473,27 +477,49 @@ class ExternalProgram:
return self._shebang_to_cmd(trial) return self._shebang_to_cmd(trial)
def _search(self, name, search_dir): 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) commands = self._search_dir(name, search_dir)
if commands: if commands:
return commands return commands
# Do a standard search in PATH # Do a standard search in PATH
fullpath = shutil.which(name) 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 # On UNIX-like platforms, the standard PATH search is enough
return [fullpath] return [fullpath]
# On Windows, if name is an absolute path, we need the extension too # HERE BEGINS THE TERROR OF WINDOWS
for ext in self.windows_exts: if fullpath:
fullpath = '{}.{}'.format(name, ext) # On Windows, even if the PATH search returned a full path, we can't be
if os.path.exists(fullpath): # 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] return [fullpath]
# On Windows, interpreted scripts must have an extension otherwise they # Try to extract the interpreter from the shebang
# cannot be found by a standard PATH search. So we do a custom search commands = self._shebang_to_cmd(fullpath)
# 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: if commands:
return 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] return [None]
def found(self): def found(self):

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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__': if __name__ == '__main__':
returncode = 0 returncode = 0
print('Running unittests.\n') print('Running unittests.\n')
units = ['InternalTests', 'AllPlatformTests']
if mesonlib.is_linux(): if mesonlib.is_linux():
returncode += subprocess.call([sys.executable, 'run_unittests.py', '-v']) units += ['LinuxlikeTests']
else: elif mesonlib.is_windows():
returncode += subprocess.call([sys.executable, 'run_unittests.py', '-v', 'InternalTests', 'AllPlatformTests']) units += ['WindowsTests']
returncode += subprocess.call([sys.executable, 'run_unittests.py', '-v'] + units)
# Ubuntu packages do not have a binary without -6 suffix. # Ubuntu packages do not have a binary without -6 suffix.
if shutil.which('arm-linux-gnueabihf-gcc-6') and not platform.machine().startswith('arm'): if shutil.which('arm-linux-gnueabihf-gcc-6') and not platform.machine().startswith('arm'):
print('Running cross compilation tests.\n') print('Running cross compilation tests.\n')

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -26,7 +26,7 @@ import mesonbuild.environment
import mesonbuild.mesonlib import mesonbuild.mesonlib
from mesonbuild.mesonlib import is_windows from mesonbuild.mesonlib import is_windows
from mesonbuild.environment import detect_ninja, Environment from mesonbuild.environment import detect_ninja, Environment
from mesonbuild.dependencies import PkgConfigDependency from mesonbuild.dependencies import PkgConfigDependency, ExternalProgram
if is_windows(): if is_windows():
exe_suffix = '.exe' exe_suffix = '.exe'
@ -186,6 +186,7 @@ class BasePlatformTests(unittest.TestCase):
super().setUp() super().setUp()
src_root = os.path.dirname(__file__) src_root = os.path.dirname(__file__)
src_root = os.path.join(os.getcwd(), src_root) 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 # In case the directory is inside a symlinked directory, find the real
# path otherwise we might not find the srcdir from inside the builddir. # path otherwise we might not find the srcdir from inside the builddir.
self.builddir = os.path.realpath(tempfile.mkdtemp()) self.builddir = os.path.realpath(tempfile.mkdtemp())
@ -564,6 +565,50 @@ class AllPlatformTests(BasePlatformTests):
self.assertPathBasenameEqual(incs[7], 'sub1') 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): class LinuxlikeTests(BasePlatformTests):
''' '''
Tests that should run on Linux and *BSD Tests that should run on Linux and *BSD

@ -1,4 +1,12 @@
project('find program', 'c') 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') prog = find_program('test-script')
test('script', prog) test('script', prog)

@ -0,0 +1,3 @@
#!/usr/bin/env python3
print('ext/noext')
Loading…
Cancel
Save