From b1df1a2bec053937ecb09f425248c1dbcee9237a Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Sun, 7 May 2017 00:20:29 +0530 Subject: [PATCH 1/2] run_command: accept built File objects too The file will always exist by the time run_command() is invoked, so there is no reason why we should forbid it. Also allow using File objects as the command to run since strings are also allowed. --- mesonbuild/interpreter.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index d8f3b2b69..2c6806a11 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -1469,28 +1469,33 @@ class Interpreter(InterpreterBase): raise InterpreterException('Not enough arguments') cmd = args[0] cargs = args[1:] + srcdir = self.environment.get_source_dir() + builddir = self.environment.get_build_dir() if isinstance(cmd, ExternalProgramHolder): cmd = cmd.get_command() elif isinstance(cmd, str): cmd = [cmd] + elif isinstance(cmd, mesonlib.File): + cmd = [cmd.absolute_path(srcdir, builddir)] else: - raise InterpreterException('First argument should be find_program() ' - 'or string, not {!r}'.format(cmd)) + m = 'First argument must be a string, or the output of ' \ + 'find_program(), files(), or configure_file(); not {!r}' + raise InterpreterException(m.format(cmd)) expanded_args = [] for a in mesonlib.flatten(cargs): if isinstance(a, str): expanded_args.append(a) elif isinstance(a, mesonlib.File): - if a.is_built: - raise InterpreterException('Can not use generated files in run_command.') - expanded_args.append(os.path.join(self.environment.get_source_dir(), str(a))) + expanded_args.append(a.absolute_path(srcdir, builddir)) else: - raise InterpreterException('Run_command arguments must be strings or the output of files().') + m = 'run_command() arguments must be strings, the output of ' \ + 'files(), or configure_file(); not {!r}' + raise InterpreterException(m.format(a)) args = cmd + expanded_args in_builddir = kwargs.get('in_builddir', False) if not isinstance(in_builddir, bool): raise InterpreterException('in_builddir must be boolean.') - return RunProcess(args, self.environment.source_dir, self.environment.build_dir, self.subdir, + return RunProcess(args, srcdir, builddir, self.subdir, get_meson_script(self.environment, 'mesonintrospect'), in_builddir) @stringArgs From 333085160d6cbff2b4a47fac76cb507cd1f6d1c7 Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Sun, 7 May 2017 01:00:25 +0530 Subject: [PATCH 2/2] run_command: Refactor + improve errors and test Refactor to use ExternalProgram for the command instead of duplicating that code (badly). Also improve messages to say "or not executable" when a script/command is not found. Also allow ExternalPrograms to be passed as arguments to run_command(). The only thing we're doing by preventing that is forcing people to use prog.path() --- mesonbuild/dependencies.py | 30 +++++---- mesonbuild/interpreter.py | 63 +++++++++---------- .../common/105 find program path/meson.build | 9 +-- .../common/105 find program path/program.py | 0 4 files changed, 49 insertions(+), 53 deletions(-) mode change 100644 => 100755 test cases/common/105 find program path/program.py diff --git a/mesonbuild/dependencies.py b/mesonbuild/dependencies.py index ef7be3a5e..37e2cbd26 100644 --- a/mesonbuild/dependencies.py +++ b/mesonbuild/dependencies.py @@ -465,8 +465,9 @@ class ExternalProgram: @staticmethod def _shebang_to_cmd(script): """ - Windows does not understand shebangs, so we check if the file has a - shebang and manually parse it to figure out the interpreter to use + Check if the file has a shebang and manually parse it to figure out + the interpreter to use. This is useful if the script is not executable + or if we're on Windows (which does not understand shebangs). """ try: with open(script) as f: @@ -504,15 +505,17 @@ class ExternalProgram: if os.path.exists(trial): if self._is_executable(trial): return [trial] + # Now getting desperate. Maybe it is a script file that is + # a) not chmodded executable, or + # b) we are on windows so they can't be directly executed. + return self._shebang_to_cmd(trial) else: - for ext in self.windows_exts: - trial_ext = '{}.{}'.format(trial, ext) - if os.path.exists(trial_ext): - return [trial_ext] - return False - # Now getting desperate. Maybe it is a script file that is a) not chmodded - # executable or b) we are on windows so they can't be directly executed. - return self._shebang_to_cmd(trial) + if mesonlib.is_windows(): + for ext in self.windows_exts: + trial_ext = '{}.{}'.format(trial, ext) + if os.path.exists(trial_ext): + return [trial_ext] + return False def _search(self, name, search_dir): ''' @@ -525,7 +528,8 @@ class ExternalProgram: # Do a standard search in PATH command = shutil.which(name) if not mesonlib.is_windows(): - # On UNIX-like platforms, the standard PATH search is enough + # On UNIX-like platforms, shutil.which() is enough to find + # all executables whether in PATH or with an absolute path return [command] # HERE BEGINS THE TERROR OF WINDOWS if command: @@ -567,9 +571,9 @@ class ExternalProgram: return self.command[:] def get_path(self): - # Assume that the last element is the full path to the script - # If it's not a script, this will be an array of length 1 if self.found(): + # Assume that the last element is the full path to the script or + # binary being run return self.command[-1] return None diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 2c6806a11..5b9bb41fe 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -23,7 +23,7 @@ from . import compilers from .wrap import wrap, WrapMode from . import mesonlib from .mesonlib import FileMode, Popen_safe, get_meson_script -from .dependencies import InternalDependency, Dependency +from .dependencies import InternalDependency, Dependency, ExternalProgram from .interpreterbase import InterpreterBase from .interpreterbase import check_stringlist, noPosargs, noKwargs, stringArgs from .interpreterbase import InterpreterException, InvalidArguments, InvalidCode @@ -72,17 +72,19 @@ class TryRunResultHolder(InterpreterObject): class RunProcess(InterpreterObject): - def __init__(self, command_array, source_dir, build_dir, subdir, mesonintrospect, in_builddir=False): + def __init__(self, cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir=False): super().__init__() - pc, self.stdout, self.stderr = self.run_command(command_array, source_dir, build_dir, subdir, mesonintrospect, in_builddir) + if not isinstance(cmd, ExternalProgram): + raise AssertionError('BUG: RunProcess must be passed an ExternalProgram') + pc, self.stdout, self.stderr = self.run_command(cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir) self.returncode = pc.returncode self.methods.update({'returncode': self.returncode_method, 'stdout': self.stdout_method, 'stderr': self.stderr_method, }) - def run_command(self, command_array, source_dir, build_dir, subdir, mesonintrospect, in_builddir): - cmd_name = command_array[0] + def run_command(self, cmd, args, source_dir, build_dir, subdir, mesonintrospect, in_builddir): + command_array = cmd.get_command() + args env = {'MESON_SOURCE_ROOT': source_dir, 'MESON_BUILD_ROOT': build_dir, 'MESON_SUBDIR': subdir, @@ -94,18 +96,6 @@ class RunProcess(InterpreterObject): child_env = os.environ.copy() child_env.update(env) mlog.debug('Running command:', ' '.join(command_array)) - try: - return Popen_safe(command_array, env=child_env, cwd=cwd) - except FileNotFoundError: - pass - # Was not a command, is a program in path? - exe = shutil.which(cmd_name) - if exe is not None: - command_array = [exe] + command_array[1:] - return Popen_safe(command_array, env=child_env, cwd=cwd) - # No? Maybe it is a script in the source tree. - fullpath = os.path.join(source_dir, subdir, cmd_name) - command_array = [fullpath] + command_array[1:] try: return Popen_safe(command_array, env=child_env, cwd=cwd) except FileNotFoundError: @@ -250,7 +240,6 @@ class DependencyHolder(InterpreterObject): def found_method(self, args, kwargs): if self.held_object.type_name == 'internal': return True - return self.held_object.found() def version_method(self, args, kwargs): @@ -1105,7 +1094,8 @@ class MesonMain(InterpreterObject): if found.found(): self._found_source_scripts[key] = found else: - raise InterpreterException('Script {!r} not found'.format(name)) + m = 'Script or command {!r} not found or not executable' + raise InterpreterException(m.format(name)) return build.RunScript(found.get_command(), args) def add_install_script_method(self, args, kwargs): @@ -1471,31 +1461,36 @@ class Interpreter(InterpreterBase): cargs = args[1:] srcdir = self.environment.get_source_dir() builddir = self.environment.get_build_dir() + m = 'must be a string, or the output of find_program(), files(), or ' \ + 'configure_file(); not {!r}' if isinstance(cmd, ExternalProgramHolder): - cmd = cmd.get_command() - elif isinstance(cmd, str): - cmd = [cmd] - elif isinstance(cmd, mesonlib.File): - cmd = [cmd.absolute_path(srcdir, builddir)] + cmd = cmd.held_object else: - m = 'First argument must be a string, or the output of ' \ - 'find_program(), files(), or configure_file(); not {!r}' - raise InterpreterException(m.format(cmd)) + if isinstance(cmd, mesonlib.File): + cmd = cmd.absolute_path(srcdir, builddir) + elif not isinstance(cmd, str): + raise InterpreterException('First argument ' + m.format(cmd)) + # Prefer scripts in the current source directory + search_dir = os.path.join(srcdir, self.subdir) + prog = ExternalProgram(cmd, silent=True, search_dir=search_dir) + if not prog.found(): + raise InterpreterException('Program or command {!r} not found' + 'or not executable'.format(cmd)) + cmd = prog expanded_args = [] for a in mesonlib.flatten(cargs): if isinstance(a, str): expanded_args.append(a) elif isinstance(a, mesonlib.File): expanded_args.append(a.absolute_path(srcdir, builddir)) + elif isinstance(a, ExternalProgramHolder): + expanded_args.append(a.held_object.get_path()) else: - m = 'run_command() arguments must be strings, the output of ' \ - 'files(), or configure_file(); not {!r}' - raise InterpreterException(m.format(a)) - args = cmd + expanded_args + raise InterpreterException('Arguments ' + m.format(a)) in_builddir = kwargs.get('in_builddir', False) if not isinstance(in_builddir, bool): raise InterpreterException('in_builddir must be boolean.') - return RunProcess(args, srcdir, builddir, self.subdir, + return RunProcess(cmd, expanded_args, srcdir, builddir, self.subdir, get_meson_script(self.environment, 'mesonintrospect'), in_builddir) @stringArgs @@ -1851,7 +1846,7 @@ class Interpreter(InterpreterBase): if progobj.found(): return progobj if required and not progobj.found(): - raise InvalidArguments('Program "%s" not found.' % exename) + raise InvalidArguments('Program "%s" not found or not executable' % exename) return progobj def func_find_library(self, node, args, kwargs): @@ -2427,7 +2422,7 @@ different subdirectory. exe_wrapper.append(i) elif isinstance(i, dependencies.ExternalProgram): if not i.found(): - raise InterpreterException('Tried to use non-found external executable.') + raise InterpreterException('Tried to use non-found executable.') exe_wrapper += i.get_command() else: raise InterpreterException('Exe wrapper can only contain strings or external binaries.') diff --git a/test cases/common/105 find program path/meson.build b/test cases/common/105 find program path/meson.build index e1e6d2e4e..0a812499a 100644 --- a/test cases/common/105 find program path/meson.build +++ b/test cases/common/105 find program path/meson.build @@ -1,9 +1,6 @@ project('find program', 'c') -python = find_program('python3', required : false) -if not python.found() - python = find_program('python') -endif +python = import('python3').find_python() # Source file via string prog = find_program('program.py') @@ -14,8 +11,8 @@ py = configure_file(input : 'program.py', output : 'builtprogram.py', configuration : configuration_data()) -foreach f : [prog, find_program(py), find_program(progf)] - ret = run_command(python, f.path()) +foreach f : [prog, progf, py, find_program(py), find_program(progf)] + ret = run_command(python, f) assert(ret.returncode() == 0, 'can\'t manually run @0@'.format(prog.path())) assert(ret.stdout().strip() == 'Found', 'wrong output from manually-run @0@'.format(prog.path())) diff --git a/test cases/common/105 find program path/program.py b/test cases/common/105 find program path/program.py old mode 100644 new mode 100755