diff options
30 files changed, 433 insertions, 393 deletions
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index e2297a3..9624ed6 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -26,6 +26,7 @@ import hashlib from .. import build from .. import dependencies +from .. import programs from .. import mesonlib from .. import mlog from ..compilers import LANGUAGES_USING_LDFLAGS @@ -140,7 +141,7 @@ class ExecutableSerialisation: self.cmd_args = cmd_args self.env = env if exe_wrapper is not None: - assert(isinstance(exe_wrapper, dependencies.ExternalProgram)) + assert(isinstance(exe_wrapper, programs.ExternalProgram)) self.exe_runner = exe_wrapper self.workdir = workdir self.extra_paths = extra_paths @@ -152,7 +153,7 @@ class ExecutableSerialisation: class TestSerialisation: def __init__(self, name: str, project: str, suite: str, fname: T.List[str], - is_cross_built: bool, exe_wrapper: T.Optional[dependencies.ExternalProgram], + is_cross_built: bool, exe_wrapper: T.Optional[programs.ExternalProgram], needs_exe_wrapper: bool, is_parallel: bool, cmd_args: T.List[str], env: build.EnvironmentVariables, should_fail: bool, timeout: T.Optional[int], workdir: T.Optional[str], @@ -164,7 +165,7 @@ class TestSerialisation: self.fname = fname self.is_cross_built = is_cross_built if exe_wrapper is not None: - assert(isinstance(exe_wrapper, dependencies.ExternalProgram)) + assert(isinstance(exe_wrapper, programs.ExternalProgram)) self.exe_runner = exe_wrapper self.is_parallel = is_parallel self.cmd_args = cmd_args @@ -406,7 +407,7 @@ class Backend: env: T.Optional[build.EnvironmentVariables] = None): exe = cmd[0] cmd_args = cmd[1:] - if isinstance(exe, dependencies.ExternalProgram): + if isinstance(exe, programs.ExternalProgram): exe_cmd = exe.get_command() exe_for_machine = exe.for_machine elif isinstance(exe, build.BuildTarget): @@ -490,7 +491,7 @@ class Backend: ['--internal', 'exe', '--capture', capture, '--'] + es.cmd_args), ', '.join(reasons)) - if isinstance(exe, (dependencies.ExternalProgram, + if isinstance(exe, (programs.ExternalProgram, build.BuildTarget, build.CustomTarget)): basename = exe.name elif isinstance(exe, mesonlib.File): @@ -897,11 +898,11 @@ class Backend: arr = [] for t in sorted(tests, key=lambda tst: -1 * tst.priority): exe = t.get_exe() - if isinstance(exe, dependencies.ExternalProgram): + if isinstance(exe, programs.ExternalProgram): cmd = exe.get_command() else: cmd = [os.path.join(self.environment.get_build_dir(), self.get_target_filename(t.get_exe()))] - if isinstance(exe, (build.BuildTarget, dependencies.ExternalProgram)): + if isinstance(exe, (build.BuildTarget, programs.ExternalProgram)): test_for_machine = exe.for_machine else: # E.g. an external verifier or simulator program run on a generated executable. diff --git a/mesonbuild/build.py b/mesonbuild/build.py index b81e5dd..547394f 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -25,6 +25,7 @@ import typing as T from . import environment from . import dependencies from . import mlog +from . import programs from .mesonlib import ( File, MesonException, MachineChoice, PerMachine, OrderedSet, listify, extract_as_list, typeslistify, stringlistify, classify_unity_sources, @@ -1486,7 +1487,7 @@ class Generator: if len(args) != 1: raise InvalidArguments('Generator requires exactly one positional argument: the executable') exe = unholder(args[0]) - if not isinstance(exe, (Executable, dependencies.ExternalProgram)): + if not isinstance(exe, (Executable, programs.ExternalProgram)): raise InvalidArguments('First generator argument must be an executable.') self.exe = exe self.depfile = None @@ -1610,7 +1611,7 @@ class GeneratedList: self.depend_files = [] self.preserve_path_from = preserve_path_from self.extra_args = extra_args if extra_args is not None else [] - if isinstance(self.generator.exe, dependencies.ExternalProgram): + if isinstance(self.generator.exe, programs.ExternalProgram): if not self.generator.exe.found(): raise InvalidArguments('Tried to use not-found external program as generator') path = self.generator.exe.get_path() @@ -2176,7 +2177,7 @@ class CommandBase: elif isinstance(c, File): self.depend_files.append(c) final_cmd.append(c) - elif isinstance(c, dependencies.ExternalProgram): + elif isinstance(c, programs.ExternalProgram): if not c.found(): raise InvalidArguments('Tried to use not-found external program in "command"') path = c.get_path() diff --git a/mesonbuild/cmake/executor.py b/mesonbuild/cmake/executor.py index 860d410..32c660a 100644 --- a/mesonbuild/cmake/executor.py +++ b/mesonbuild/cmake/executor.py @@ -23,12 +23,13 @@ import re import os from .. import mlog +from ..environment import Environment from ..mesonlib import PerMachine, Popen_safe, version_compare, MachineChoice, is_windows, OptionKey +from ..programs import find_external_program, NonExistingExternalProgram if T.TYPE_CHECKING: from ..environment import Environment - from ..dependencies.base import ExternalProgram - from ..compilers import Compiler + from ..programs import ExternalProgram TYPE_result = T.Tuple[int, T.Optional[str], T.Optional[str]] TYPE_cache_key = T.Tuple[str, T.Tuple[str, ...], str, T.FrozenSet[T.Tuple[str, str]]] @@ -65,9 +66,7 @@ class CMakeExecutor: if self.prefix_paths: self.extra_cmake_args += ['-DCMAKE_PREFIX_PATH={}'.format(';'.join(self.prefix_paths))] - def find_cmake_binary(self, environment: 'Environment', silent: bool = False) -> T.Tuple[T.Optional['ExternalProgram'], T.Optional[str]]: - from ..dependencies.base import find_external_program, NonExistingExternalProgram - + def find_cmake_binary(self, environment: Environment, silent: bool = False) -> T.Tuple[T.Optional['ExternalProgram'], T.Optional[str]]: # Only search for CMake the first time and store the result in the class # definition if isinstance(CMakeExecutor.class_cmakebin[self.for_machine], NonExistingExternalProgram): diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py index cccd358..130723f 100644 --- a/mesonbuild/cmake/interpreter.py +++ b/mesonbuild/cmake/interpreter.py @@ -25,6 +25,7 @@ from .. import mlog, mesonlib from ..mesonlib import MachineChoice, OrderedSet, version_compare, path_is_in_root, relative_to_if_possible, OptionKey from ..mesondata import mesondata from ..compilers.compilers import lang_suffixes, header_suffixes, obj_suffixes, lib_suffixes, is_header +from ..programs import ExternalProgram from enum import Enum from functools import lru_cache from pathlib import Path @@ -750,7 +751,6 @@ class ConverterCustomTarget: if target: # When cross compiling, binaries have to be executed with an exe_wrapper (for instance wine for mingw-w64) if self.env.exe_wrapper is not None and self.env.properties[self.for_machine].get_cmake_use_exe_wrapper(): - from ..dependencies import ExternalProgram assert isinstance(self.env.exe_wrapper, ExternalProgram) cmd += self.env.exe_wrapper.get_command() cmd += [target] diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py index 759b969..15b0e29 100644 --- a/mesonbuild/compilers/c.py +++ b/mesonbuild/compilers/c.py @@ -40,10 +40,11 @@ from .compilers import ( if T.TYPE_CHECKING: from ..coredata import KeyedOptionDictType - from ..dependencies import Dependency, ExternalProgram + from ..dependencies import Dependency from ..envconfig import MachineInfo from ..environment import Environment from ..linkers import DynamicLinker + from ..programs import ExternalProgram CompilerMixinBase = Compiler else: diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py index d0e3fd2..c267c0f 100644 --- a/mesonbuild/compilers/cpp.py +++ b/mesonbuild/compilers/cpp.py @@ -42,10 +42,11 @@ from .mixins.emscripten import EmscriptenMixin if T.TYPE_CHECKING: from ..coredata import KeyedOptionDictType - from ..dependencies import Dependency, ExternalProgram + from ..dependencies import Dependency from ..envconfig import MachineInfo from ..environment import Environment from ..linkers import DynamicLinker + from ..programs import ExternalProgram from .mixins.clike import CLikeCompiler as CompilerMixinBase else: CompilerMixinBase = object diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py index b7dc0f5..ef6375a 100644 --- a/mesonbuild/compilers/cuda.py +++ b/mesonbuild/compilers/cuda.py @@ -29,10 +29,11 @@ from .compilers import (Compiler, cuda_buildtype_args, cuda_optimization_args, if T.TYPE_CHECKING: from ..build import BuildTarget from ..coredata import KeyedOptionDictType - from ..dependencies import Dependency, ExternalProgram + from ..dependencies import Dependency from ..environment import Environment # noqa: F401 from ..envconfig import MachineInfo from ..linkers import DynamicLinker + from ..programs import ExternalProgram class _Phase(enum.Enum): diff --git a/mesonbuild/compilers/d.py b/mesonbuild/compilers/d.py index 837ee9a..78d0f62 100644 --- a/mesonbuild/compilers/d.py +++ b/mesonbuild/compilers/d.py @@ -34,7 +34,7 @@ from .mixins.gnu import GnuCompiler if T.TYPE_CHECKING: from .compilers import Compiler as CompilerMixinBase - from ..dependencies import Dependency, ExternalProgram + from ..programs import ExternalProgram from ..envconfig import MachineInfo from ..environment import Environment from ..linkers import DynamicLinker diff --git a/mesonbuild/compilers/fortran.py b/mesonbuild/compilers/fortran.py index d65d585..0cff60a 100644 --- a/mesonbuild/compilers/fortran.py +++ b/mesonbuild/compilers/fortran.py @@ -37,10 +37,11 @@ from mesonbuild.mesonlib import ( if T.TYPE_CHECKING: from ..coredata import KeyedOptionDictType - from ..dependencies import Dependency, ExternalProgram + from ..dependencies import Dependency from ..envconfig import MachineInfo from ..environment import Environment from ..linkers import DynamicLinker + from ..programs import ExternalProgram class FortranCompiler(CLikeCompiler, Compiler): diff --git a/mesonbuild/compilers/mixins/clike.py b/mesonbuild/compilers/mixins/clike.py index 787c2c1..3932244 100644 --- a/mesonbuild/compilers/mixins/clike.py +++ b/mesonbuild/compilers/mixins/clike.py @@ -41,9 +41,10 @@ from ..compilers import CompileCheckMode from .visualstudio import VisualStudioLikeCompiler if T.TYPE_CHECKING: - from ...dependencies import Dependency, ExternalProgram + from ...dependencies import Dependency from ...environment import Environment from ...compilers.compilers import Compiler + from ...programs import ExternalProgram else: # This is a bit clever, for mypy we pretend that these mixins descend from # Compiler, so we get all of the methods and attributes defined for us, but diff --git a/mesonbuild/compilers/objc.py b/mesonbuild/compilers/objc.py index e47bf2f..ce9cf2d 100644 --- a/mesonbuild/compilers/objc.py +++ b/mesonbuild/compilers/objc.py @@ -23,7 +23,7 @@ from .mixins.gnu import GnuCompiler from .mixins.clang import ClangCompiler if T.TYPE_CHECKING: - from ..dependencies import ExternalProgram + from ..programs import ExternalProgram from ..envconfig import MachineInfo from ..environment import Environment from ..linkers import DynamicLinker diff --git a/mesonbuild/compilers/objcpp.py b/mesonbuild/compilers/objcpp.py index c0f93d7..585a45e 100644 --- a/mesonbuild/compilers/objcpp.py +++ b/mesonbuild/compilers/objcpp.py @@ -23,7 +23,7 @@ from .mixins.gnu import GnuCompiler from .mixins.clang import ClangCompiler if T.TYPE_CHECKING: - from ..dependencies import ExternalProgram + from ..programs import ExternalProgram from ..envconfig import MachineInfo from ..environment import Environment from ..linkers import DynamicLinker diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py index 7354d58..285d490 100644 --- a/mesonbuild/compilers/rust.py +++ b/mesonbuild/compilers/rust.py @@ -25,10 +25,10 @@ from .compilers import Compiler, rust_buildtype_args, clike_debug_args if T.TYPE_CHECKING: from ..coredata import KeyedOptionDictType - from ..dependencies import ExternalProgram from ..envconfig import MachineInfo from ..environment import Environment # noqa: F401 from ..linkers import DynamicLinker + from ..programs import ExternalProgram rust_optimization_args = { diff --git a/mesonbuild/dependencies/__init__.py b/mesonbuild/dependencies/__init__.py index dc1b290c..91d3d87 100644 --- a/mesonbuild/dependencies/__init__.py +++ b/mesonbuild/dependencies/__init__.py @@ -16,10 +16,11 @@ from .boost import BoostDependency from .cuda import CudaDependency from .hdf5 import hdf5_factory from .base import ( # noqa: F401 - Dependency, DependencyException, DependencyMethods, ExternalProgram, EmptyExternalProgram, NonExistingExternalProgram, - ExternalDependency, NotFoundDependency, ExternalLibrary, ExtraFrameworkDependency, InternalDependency, - PkgConfigDependency, CMakeDependency, find_external_dependency, get_dep_identifier, packages, _packages_accept_language, - DependencyFactory) + Dependency, DependencyException, DependencyMethods, ExternalDependency, + NotFoundDependency, ExternalLibrary, ExtraFrameworkDependency, + InternalDependency, PkgConfigDependency, CMakeDependency, + find_external_dependency, get_dep_identifier, packages, + _packages_accept_language, DependencyFactory) from .dev import ValgrindDependency, gmock_factory, gtest_factory, llvm_factory, zlib_factory from .coarrays import coarray_factory from .mpi import mpi_factory diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py index 0814218..27c33b6 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -21,8 +21,6 @@ import re import json import shlex import shutil -import stat -import sys import textwrap import platform import typing as T @@ -38,6 +36,7 @@ from ..mesonlib import MachineChoice, MesonException, OrderedSet, PerMachine from ..mesonlib import Popen_safe, version_compare_many, version_compare, listify, stringlistify, extract_as_list, split_args from ..mesonlib import Version, LibType, OptionKey from ..mesondata import mesondata +from ..programs import ExternalProgram, find_external_program if T.TYPE_CHECKING: from ..compilers.compilers import CompilerType # noqa: F401 @@ -75,30 +74,6 @@ class DependencyMethods(Enum): DUB = 'dub' -def find_external_program(env: Environment, for_machine: MachineChoice, name: str, - display_name: str, default_names: T.List[str], - allow_default_for_cross: bool = True) -> T.Generator['ExternalProgram', None, None]: - """Find an external program, chcking the cross file plus any default options.""" - # Lookup in cross or machine file. - potential_path = env.lookup_binary_entry(for_machine, name) - if potential_path is not None: - mlog.debug('{} binary for {} specified from cross file, native file, ' - 'or env var as {}'.format(display_name, for_machine, potential_path)) - yield ExternalProgram.from_entry(name, potential_path) - # We never fallback if the user-specified option is no good, so - # stop returning options. - return - mlog.debug(f'{display_name} binary missing from cross or native file, or env var undefined.') - # Fallback on hard-coded defaults, if a default binary is allowed for use - # with cross targets, or if this is not a cross target - if allow_default_for_cross or not (for_machine is MachineChoice.HOST and env.is_cross_build(for_machine)): - for potential_path in default_names: - mlog.debug(f'Trying a default {display_name} fallback at', potential_path) - yield ExternalProgram(potential_path, silent=True) - else: - mlog.debug('Default target is not allowed for cross use') - - class Dependency: @classmethod @@ -1852,303 +1827,6 @@ class DubDependency(ExternalDependency): def get_methods(): return [DependencyMethods.DUB] -class ExternalProgram: - windows_exts = ('exe', 'msc', 'com', 'bat', 'cmd') - # An 'ExternalProgram' always runs on the build machine - for_machine = MachineChoice.BUILD - - def __init__(self, name: str, command: T.Optional[T.List[str]] = None, - silent: bool = False, search_dir: T.Optional[str] = None, - extra_search_dirs: T.Optional[T.List[str]] = None): - self.name = name - if command is not None: - self.command = listify(command) - if mesonlib.is_windows(): - cmd = self.command[0] - args = self.command[1:] - # Check whether the specified cmd is a path to a script, in - # which case we need to insert the interpreter. If not, try to - # use it as-is. - ret = self._shebang_to_cmd(cmd) - if ret: - self.command = ret + args - else: - self.command = [cmd] + args - else: - all_search_dirs = [search_dir] - if extra_search_dirs: - all_search_dirs += extra_search_dirs - for d in all_search_dirs: - self.command = self._search(name, d) - if self.found(): - break - - # Set path to be the last item that is actually a file (in order to - # skip options in something like ['python', '-u', 'file.py']. If we - # can't find any components, default to the last component of the path. - self.path = self.command[-1] - for i in range(len(self.command) - 1, -1, -1): - arg = self.command[i] - if arg is not None and os.path.isfile(arg): - self.path = arg - break - - if not silent: - # ignore the warning because derived classes never call this __init__ - # method, and thus only the found() method of this class is ever executed - if self.found(): # lgtm [py/init-calls-subclass] - mlog.log('Program', mlog.bold(name), 'found:', mlog.green('YES'), - '(%s)' % ' '.join(self.command)) - else: - mlog.log('Program', mlog.bold(name), 'found:', mlog.red('NO')) - - def summary_value(self) -> T.Union[str, mlog.AnsiDecorator]: - if not self.found(): - return mlog.red('NO') - return self.path - - def __repr__(self) -> str: - r = '<{} {!r} -> {!r}>' - return r.format(self.__class__.__name__, self.name, self.command) - - def description(self) -> str: - '''Human friendly description of the command''' - return ' '.join(self.command) - - @classmethod - def from_bin_list(cls, env: Environment, for_machine: MachineChoice, name): - # There is a static `for_machine` for this class because the binary - # aways runs on the build platform. (It's host platform is our build - # platform.) But some external programs have a target platform, so this - # is what we are specifying here. - command = env.lookup_binary_entry(for_machine, name) - if command is None: - return NonExistingExternalProgram() - return cls.from_entry(name, command) - - @staticmethod - @functools.lru_cache(maxsize=None) - def _windows_sanitize_path(path: str) -> str: - # Ensure that we use USERPROFILE even when inside MSYS, MSYS2, Cygwin, etc. - if 'USERPROFILE' not in os.environ: - return path - # The WindowsApps directory is a bit of a problem. It contains - # some zero-sized .exe files which have "reparse points", that - # might either launch an installed application, or might open - # a page in the Windows Store to download the application. - # - # To handle the case where the python interpreter we're - # running on came from the Windows Store, if we see the - # WindowsApps path in the search path, replace it with - # dirname(sys.executable). - appstore_dir = Path(os.environ['USERPROFILE']) / 'AppData' / 'Local' / 'Microsoft' / 'WindowsApps' - paths = [] - for each in path.split(os.pathsep): - if Path(each) != appstore_dir: - paths.append(each) - elif 'WindowsApps' in sys.executable: - paths.append(os.path.dirname(sys.executable)) - return os.pathsep.join(paths) - - @staticmethod - def from_entry(name, command): - if isinstance(command, list): - if len(command) == 1: - command = command[0] - # We cannot do any searching if the command is a list, and we don't - # need to search if the path is an absolute path. - if isinstance(command, list) or os.path.isabs(command): - return ExternalProgram(name, command=command, silent=True) - assert isinstance(command, str) - # Search for the command using the specified string! - return ExternalProgram(command, silent=True) - - @staticmethod - def _shebang_to_cmd(script: str) -> T.Optional[list]: - """ - 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: - first_line = f.readline().strip() - if first_line.startswith('#!'): - # In a shebang, everything before the first space is assumed to - # be the command to run and everything after the first space is - # the single argument to pass to that command. So we must split - # exactly once. - commands = first_line[2:].split('#')[0].strip().split(maxsplit=1) - if mesonlib.is_windows(): - # Windows does not have UNIX paths so remove them, - # but don't remove Windows paths - if commands[0].startswith('/'): - commands[0] = commands[0].split('/')[-1] - if len(commands) > 0 and commands[0] == 'env': - commands = commands[1:] - # Windows does not ship python3.exe, but we know the path to it - if len(commands) > 0 and commands[0] == 'python3': - commands = mesonlib.python_command + commands[1:] - elif mesonlib.is_haiku(): - # Haiku does not have /usr, but a lot of scripts assume that - # /usr/bin/env always exists. Detect that case and run the - # script with the interpreter after it. - if commands[0] == '/usr/bin/env': - commands = commands[1:] - # We know what python3 is, we're running on it - if len(commands) > 0 and commands[0] == 'python3': - commands = mesonlib.python_command + commands[1:] - else: - # Replace python3 with the actual python3 that we are using - if commands[0] == '/usr/bin/env' and commands[1] == 'python3': - commands = mesonlib.python_command + commands[2:] - elif commands[0].split('/')[-1] == 'python3': - commands = mesonlib.python_command + commands[1:] - return commands + [script] - except Exception as e: - mlog.debug(e) - mlog.debug(f'Unusable script {script!r}') - return None - - def _is_executable(self, path): - suffix = os.path.splitext(path)[-1].lower()[1:] - execmask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH - if mesonlib.is_windows(): - if suffix in self.windows_exts: - return True - elif os.stat(path).st_mode & execmask: - return not os.path.isdir(path) - return False - - def _search_dir(self, name: str, search_dir: T.Optional[str]) -> T.Optional[list]: - if search_dir is None: - return None - trial = os.path.join(search_dir, name) - 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: - if mesonlib.is_windows(): - for ext in self.windows_exts: - trial_ext = f'{trial}.{ext}' - if os.path.exists(trial_ext): - return [trial_ext] - return None - - def _search_windows_special_cases(self, name: str, command: str) -> list: - ''' - Lots of weird Windows quirks: - 1. PATH search for @name returns files with extensions from PATHEXT, - but only self.windows_exts are executable without an interpreter. - 2. @name might be an absolute path to an executable, but without the - extension. This works inside MinGW so people use it a lot. - 3. The script is specified without an extension, in which case we have - to manually search in PATH. - 4. More special-casing for the shebang inside the script. - ''' - if command: - # 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(command)[1] - if name_ext[1:].lower() in self.windows_exts: - # Good, it can be directly executed - return [command] - # Try to extract the interpreter from the shebang - commands = self._shebang_to_cmd(command) - if commands: - return commands - return [None] - # 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: - command = f'{name}.{ext}' - if os.path.exists(command): - return [command] - # 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 = self._windows_sanitize_path(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 _search(self, name: str, search_dir: T.Optional[str]) -> list: - ''' - 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 - path = os.environ.get('PATH', None) - if mesonlib.is_windows() and path: - path = self._windows_sanitize_path(path) - command = shutil.which(name, path=path) - if mesonlib.is_windows(): - return self._search_windows_special_cases(name, command) - # On UNIX-like platforms, shutil.which() is enough to find - # all executables whether in PATH or with an absolute path - return [command] - - def found(self) -> bool: - return self.command[0] is not None - - def get_command(self) -> T.List[str]: - return self.command[:] - - def get_path(self) -> str: - return self.path - - def get_name(self) -> str: - return self.name - - -class NonExistingExternalProgram(ExternalProgram): # lgtm [py/missing-call-to-init] - "A program that will never exist" - - def __init__(self, name: str = 'nonexistingprogram') -> None: - self.name = name - self.command = [None] - self.path = None - - def __repr__(self) -> str: - r = '<{} {!r} -> {!r}>' - return r.format(self.__class__.__name__, self.name, self.command) - - def found(self) -> bool: - return False - - -class EmptyExternalProgram(ExternalProgram): # lgtm [py/missing-call-to-init] - ''' - A program object that returns an empty list of commands. Used for cases - such as a cross file exe_wrapper to represent that it's not required. - ''' - - def __init__(self): - self.name = None - self.command = [] - self.path = None - - def __repr__(self): - r = '<{} {!r} -> {!r}>' - return r.format(self.__class__.__name__, self.name, self.command) - - def found(self): - return True - - class ExternalLibrary(ExternalDependency): def __init__(self, name, link_args, environment, language, silent=False): super().__init__('library', environment, {}, language=language) diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py index e323073..40455b8 100644 --- a/mesonbuild/dependencies/ui.py +++ b/mesonbuild/dependencies/ui.py @@ -28,14 +28,14 @@ from ..mesonlib import ( from ..environment import detect_cpu_family from .base import DependencyException, DependencyMethods -from .base import ExternalDependency, NonExistingExternalProgram +from .base import ExternalDependency from .base import ExtraFrameworkDependency, PkgConfigDependency from .base import ConfigToolDependency, DependencyFactory -from .base import find_external_program +from ..programs import find_external_program, NonExistingExternalProgram if T.TYPE_CHECKING: from ..environment import Environment - from .base import ExternalProgram + from ..programs import ExternalProgram class GLDependencySystem(ExternalDependency): diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index 620e0d2..f59eb87 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -27,6 +27,9 @@ from .mesonlib import ( PerMachineDefaultable, PerThreeMachineDefaultable, split_args, quote_arg, OptionKey ) from . import mlog +from .programs import ( + ExternalProgram, EmptyExternalProgram +) from .envconfig import ( BinaryTable, MachineInfo, Properties, known_cpu_families, CMakeVariables, @@ -208,7 +211,6 @@ def detect_ninja(version: str = '1.8.2', log: bool = False) -> T.List[str]: return r[0] if r else None def detect_ninja_command_and_version(version: str = '1.8.2', log: bool = False) -> (T.List[str], str): - from .dependencies.base import ExternalProgram env_ninja = os.environ.get('NINJA', None) for n in [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu']: prog = ExternalProgram(n, silent=True) @@ -694,7 +696,6 @@ class Environment: exe_wrapper = self.lookup_binary_entry(MachineChoice.HOST, 'exe_wrapper') if exe_wrapper is not None: - from .dependencies import ExternalProgram self.exe_wrapper = ExternalProgram.from_bin_list(self, MachineChoice.HOST, 'exe_wrapper') else: self.exe_wrapper = None @@ -2157,6 +2158,5 @@ class Environment: def get_exe_wrapper(self): if not self.need_exe_wrapper(): - from .dependencies import EmptyExternalProgram return EmptyExternalProgram() return self.exe_wrapper diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 6a51f3d..f4e296d 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -22,7 +22,7 @@ from . import compilers from .wrap import wrap, WrapMode from . import mesonlib from .mesonlib import FileMode, MachineChoice, OptionKey, Popen_safe, listify, extract_as_list, has_path_sep, unholder -from .dependencies import ExternalProgram +from .programs import ExternalProgram, NonExistingExternalProgram from .dependencies import InternalDependency, Dependency, NotFoundDependency, DependencyException from .depfile import DepFile from .interpreterbase import InterpreterBase, typed_pos_args @@ -72,7 +72,7 @@ def stringifyUserArguments(args, quote=False): raise InvalidArguments('Function accepts only strings, integers, lists, dictionaries and lists thereof.') -class OverrideProgram(dependencies.ExternalProgram): +class OverrideProgram(ExternalProgram): pass @@ -1931,9 +1931,9 @@ class MesonMain(InterpreterObject): found = self._found_source_scripts[key] elif isinstance(prog, mesonlib.File): prog = prog.rel_to_builddir(self.interpreter.environment.source_dir) - found = dependencies.ExternalProgram(prog, search_dir=self.interpreter.environment.build_dir) + found = ExternalProgram(prog, search_dir=self.interpreter.environment.build_dir) else: - found = dependencies.ExternalProgram(prog, search_dir=search_dir) + found = ExternalProgram(prog, search_dir=search_dir) if found.found(): self._found_source_scripts[key] = found @@ -1976,7 +1976,7 @@ class MesonMain(InterpreterObject): elif isinstance(a, build.ConfigureFile): new = True script_args.append(os.path.join(a.subdir, a.targetname)) - elif isinstance(a, dependencies.ExternalProgram): + elif isinstance(a, ExternalProgram): script_args.extend(a.command) new = True else: @@ -2163,7 +2163,7 @@ class MesonMain(InterpreterObject): if not os.path.exists(abspath): raise InterpreterException('Tried to override %s with a file that does not exist.' % name) exe = OverrideProgram(name, abspath) - if not isinstance(exe, (dependencies.ExternalProgram, build.Executable)): + if not isinstance(exe, (ExternalProgram, build.Executable)): raise InterpreterException('Second argument must be an external program or executable.') self.interpreter.add_find_program_override(name, exe) @@ -2543,7 +2543,7 @@ class Interpreter(InterpreterBase): return DataHolder(item) elif isinstance(item, dependencies.Dependency): return DependencyHolder(item, self.subproject) - elif isinstance(item, dependencies.ExternalProgram): + elif isinstance(item, ExternalProgram): return ExternalProgramHolder(item, self.subproject) elif isinstance(item, ModuleObject): return ModuleObjectHolder(item, self) @@ -2576,7 +2576,7 @@ class Interpreter(InterpreterBase): elif isinstance(v, Test): self.build.tests.append(v) elif isinstance(v, (int, str, bool, Disabler, ObjectHolder, build.GeneratedList, - dependencies.ExternalProgram)): + ExternalProgram)): pass else: raise InterpreterException('Module returned a value of unknown type.') @@ -3424,12 +3424,12 @@ external dependencies (including libraries) must go to "dependencies".''') else: raise InvalidArguments('find_program only accepts strings and ' 'files, not {!r}'.format(exename)) - extprog = dependencies.ExternalProgram(exename, search_dir=search_dir, - extra_search_dirs=extra_search_dirs, - silent=True) + extprog = ExternalProgram(exename, search_dir=search_dir, + extra_search_dirs=extra_search_dirs, + silent=True) progobj = ExternalProgramHolder(extprog, self.subproject) if progobj.found(): - extra_info.append('({})'.format(' '.join(progobj.get_command()))) + extra_info.append(f"({' '.join(progobj.get_command())})") return progobj def program_from_overrides(self, command_names, extra_info): @@ -3457,7 +3457,7 @@ external dependencies (including libraries) must go to "dependencies".''') self.build.find_overrides[name] = exe def notfound_program(self, args): - return ExternalProgramHolder(dependencies.NonExistingExternalProgram(' '.join(args)), self.subproject) + return ExternalProgramHolder(NonExistingExternalProgram(' '.join(args)), self.subproject) # TODO update modules to always pass `for_machine`. It is bad-form to assume # the host machine. @@ -3515,7 +3515,7 @@ external dependencies (including libraries) must go to "dependencies".''') if progobj is None: progobj = self.program_from_system(args, search_dirs, extra_info) if progobj is None and args[0].endswith('python3'): - prog = dependencies.ExternalProgram('python3', mesonlib.python_command, silent=True) + prog = ExternalProgram('python3', mesonlib.python_command, silent=True) progobj = ExternalProgramHolder(prog, self.subproject) if prog.found() else None if progobj is None and fallback and required: progobj = self.find_program_fallback(fallback, args, required, extra_info) @@ -4032,10 +4032,10 @@ This will become a hard error in the future.''' % kwargs['input'], location=self cleaned_args = [] for i in unholder(listify(all_args)): - if not isinstance(i, (str, build.BuildTarget, build.CustomTarget, dependencies.ExternalProgram, mesonlib.File)): + if not isinstance(i, (str, build.BuildTarget, build.CustomTarget, ExternalProgram, mesonlib.File)): mlog.debug('Wrong type:', str(i)) raise InterpreterException('Invalid argument to run_target.') - if isinstance(i, dependencies.ExternalProgram) and not i.found(): + if isinstance(i, ExternalProgram) and not i.found(): raise InterpreterException(f'Tried to use non-existing executable {i.name!r}') cleaned_args.append(i) if isinstance(cleaned_args[0], str): @@ -4647,7 +4647,7 @@ This warning will become a hard error in a future Meson release. for i in inp: if isinstance(i, str): exe_wrapper.append(i) - elif isinstance(i, dependencies.ExternalProgram): + elif isinstance(i, ExternalProgram): if not i.found(): raise InterpreterException('Tried to use non-found executable.') exe_wrapper += i.get_command() diff --git a/mesonbuild/modules/cmake.py b/mesonbuild/modules/cmake.py index fd92ecf..84fe658 100644 --- a/mesonbuild/modules/cmake.py +++ b/mesonbuild/modules/cmake.py @@ -18,7 +18,7 @@ import typing as T from . import ExtensionModule, ModuleReturnValue -from .. import build, dependencies, mesonlib, mlog +from .. import build, mesonlib, mlog from ..cmake import SingleTargetOptions, TargetOptions, cmake_defines_to_args from ..interpreter import ConfigurationDataHolder, InterpreterException, SubprojectHolder, DependencyHolder from ..interpreterbase import ( @@ -36,6 +36,7 @@ from ..interpreterbase import ( InvalidArguments, ) +from ..programs import ExternalProgram COMPATIBILITIES = ['AnyNewerVersion', 'SameMajorVersion', 'SameMinorVersion', 'ExactVersion'] @@ -232,7 +233,7 @@ class CmakeModule(ExtensionModule): if self.cmake_detected: return True - cmakebin = dependencies.ExternalProgram('cmake', silent=False) + cmakebin = ExternalProgram('cmake', silent=False) p, stdout, stderr = mesonlib.Popen_safe(cmakebin.get_command() + ['--system-information', '-G', 'Ninja'])[0:3] if p.returncode != 0: mlog.log(f'error retrieving cmake information: returnCode={p.returncode} stdout={stdout} stderr={stderr}') diff --git a/mesonbuild/modules/dlang.py b/mesonbuild/modules/dlang.py index 55ff304..f2633cb 100644 --- a/mesonbuild/modules/dlang.py +++ b/mesonbuild/modules/dlang.py @@ -26,10 +26,8 @@ from ..mesonlib import ( Popen_safe, MesonException ) -from ..dependencies.base import ( - ExternalProgram, DubDependency -) - +from ..dependencies.base import DubDependency +from ..programs import ExternalProgram from ..interpreter import DependencyHolder class DlangModule(ExtensionModule): diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py index f1df18a..6ca866f 100644 --- a/mesonbuild/modules/gnome.py +++ b/mesonbuild/modules/gnome.py @@ -33,7 +33,8 @@ from ..mesonlib import ( MachineChoice, MesonException, OrderedSet, Popen_safe, extract_as_list, join_args, unholder, ) -from ..dependencies import Dependency, PkgConfigDependency, InternalDependency, ExternalProgram +from ..dependencies import Dependency, PkgConfigDependency, InternalDependency +from ..programs import ExternalProgram from ..interpreterbase import noPosargs, noKwargs, permittedKwargs, FeatureNew, FeatureNewKwargs, FeatureDeprecatedKwargs if T.TYPE_CHECKING: diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py index 51da31c..bf8cd22 100644 --- a/mesonbuild/modules/hotdoc.py +++ b/mesonbuild/modules/hotdoc.py @@ -23,9 +23,10 @@ from mesonbuild.coredata import MesonException from . import ModuleReturnValue from . import ExtensionModule from . import get_include_args -from ..dependencies import Dependency, InternalDependency, ExternalProgram +from ..dependencies import Dependency, InternalDependency from ..interpreterbase import FeatureNew, InvalidArguments, noPosargs, noKwargs from ..interpreter import CustomTargetHolder +from ..programs import ExternalProgram def ensure_list(value): diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index 122f977..f46f00e 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -32,9 +32,9 @@ from .. import mlog from ..environment import detect_cpu_family from ..dependencies.base import ( DependencyMethods, ExternalDependency, - ExternalProgram, PkgConfigDependency, - NonExistingExternalProgram, NotFoundDependency + PkgConfigDependency, NotFoundDependency ) +from ..programs import ExternalProgram, NonExistingExternalProgram mod_kwargs = {'subdir'} mod_kwargs.update(known_shmod_kwargs) diff --git a/mesonbuild/modules/python3.py b/mesonbuild/modules/python3.py index 8815966..e7a2bb3 100644 --- a/mesonbuild/modules/python3.py +++ b/mesonbuild/modules/python3.py @@ -13,12 +13,13 @@ # limitations under the License. import sysconfig -from .. import mesonlib, dependencies +from .. import mesonlib from . import ExtensionModule from mesonbuild.modules import ModuleReturnValue from ..interpreterbase import noKwargs, permittedKwargs, FeatureDeprecated from ..build import known_shmod_kwargs +from ..programs import ExternalProgram class Python3Module(ExtensionModule): @@ -50,9 +51,9 @@ class Python3Module(ExtensionModule): def find_python(self, state, args, kwargs): command = state.environment.lookup_binary_entry(mesonlib.MachineChoice.HOST, 'python3') if command is not None: - py3 = dependencies.ExternalProgram.from_entry('python3', command) + py3 = ExternalProgram.from_entry('python3', command) else: - py3 = dependencies.ExternalProgram('python3', mesonlib.python_command, silent=True) + py3 = ExternalProgram('python3', mesonlib.python_command, silent=True) return ModuleReturnValue(py3, [py3]) @noKwargs diff --git a/mesonbuild/modules/qt.py b/mesonbuild/modules/qt.py index 32841ff..de3a5b1 100644 --- a/mesonbuild/modules/qt.py +++ b/mesonbuild/modules/qt.py @@ -17,11 +17,12 @@ import shutil from .. import mlog from .. import build from ..mesonlib import MesonException, extract_as_list, File, unholder, version_compare -from ..dependencies import Dependency, Qt4Dependency, Qt5Dependency, Qt6Dependency, NonExistingExternalProgram +from ..dependencies import Dependency, Qt4Dependency, Qt5Dependency, Qt6Dependency import xml.etree.ElementTree as ET from . import ModuleReturnValue, get_include_args, ExtensionModule from ..interpreterbase import noPosargs, permittedKwargs, FeatureNew, FeatureNewKwargs from ..interpreter import extract_required_kwarg +from ..programs import NonExistingExternalProgram _QT_DEPS_LUT = { 4: Qt4Dependency, diff --git a/mesonbuild/modules/unstable_rust.py b/mesonbuild/modules/unstable_rust.py index 11b4365..91f7146 100644 --- a/mesonbuild/modules/unstable_rust.py +++ b/mesonbuild/modules/unstable_rust.py @@ -26,7 +26,7 @@ from ..mesonlib import stringlistify, unholder, listify, typeslistify, File if T.TYPE_CHECKING: from . import ModuleState from ..interpreter import Interpreter - from ..dependencies import ExternalProgram + from ..programs import ExternalProgram class RustModule(ExtensionModule): diff --git a/mesonbuild/modules/windows.py b/mesonbuild/modules/windows.py index 54bd265..d7a8638 100644 --- a/mesonbuild/modules/windows.py +++ b/mesonbuild/modules/windows.py @@ -24,7 +24,7 @@ from . import ModuleReturnValue from . import ExtensionModule from ..interpreter import CustomTargetHolder from ..interpreterbase import permittedKwargs, FeatureNewKwargs, flatten -from ..dependencies import ExternalProgram +from ..programs import ExternalProgram class ResourceCompilerType(enum.Enum): windres = 1 diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index c48a324..42963ff 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -43,9 +43,9 @@ from . import environment from . import mlog from .coredata import major_versions_differ, MesonVersionMismatchException from .coredata import version as coredata_version -from .dependencies import ExternalProgram from .mesonlib import MesonException, OrderedSet, get_wine_shortpath, split_args, join_args from .mintro import get_infodir, load_info_file +from .programs import ExternalProgram from .backend.backends import TestProtocol, TestSerialisation # GNU autotools interprets a return code of 77 from tests it executes to diff --git a/mesonbuild/programs.py b/mesonbuild/programs.py new file mode 100644 index 0000000..0bfe773 --- /dev/null +++ b/mesonbuild/programs.py @@ -0,0 +1,351 @@ +# Copyright 2013-2020 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. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Representations and logic for External and Internal Programs.""" + +import functools +import os +import shutil +import stat +import sys +import typing as T +from pathlib import Path + +from . import mesonlib +from . import mlog +from .mesonlib import MachineChoice + +if T.TYPE_CHECKING: + from .environment import Environment + + +class ExternalProgram: + windows_exts = ('exe', 'msc', 'com', 'bat', 'cmd') + # An 'ExternalProgram' always runs on the build machine + for_machine = MachineChoice.BUILD + + def __init__(self, name: str, command: T.Optional[T.List[str]] = None, + silent: bool = False, search_dir: T.Optional[str] = None, + extra_search_dirs: T.Optional[T.List[str]] = None): + self.name = name + if command is not None: + self.command = mesonlib.listify(command) + if mesonlib.is_windows(): + cmd = self.command[0] + args = self.command[1:] + # Check whether the specified cmd is a path to a script, in + # which case we need to insert the interpreter. If not, try to + # use it as-is. + ret = self._shebang_to_cmd(cmd) + if ret: + self.command = ret + args + else: + self.command = [cmd] + args + else: + all_search_dirs = [search_dir] + if extra_search_dirs: + all_search_dirs += extra_search_dirs + for d in all_search_dirs: + self.command = self._search(name, d) + if self.found(): + break + + # Set path to be the last item that is actually a file (in order to + # skip options in something like ['python', '-u', 'file.py']. If we + # can't find any components, default to the last component of the path. + self.path = self.command[-1] + for i in range(len(self.command) - 1, -1, -1): + arg = self.command[i] + if arg is not None and os.path.isfile(arg): + self.path = arg + break + + if not silent: + # ignore the warning because derived classes never call this __init__ + # method, and thus only the found() method of this class is ever executed + if self.found(): # lgtm [py/init-calls-subclass] + mlog.log('Program', mlog.bold(name), 'found:', mlog.green('YES'), + '(%s)' % ' '.join(self.command)) + else: + mlog.log('Program', mlog.bold(name), 'found:', mlog.red('NO')) + + def summary_value(self) -> T.Union[str, mlog.AnsiDecorator]: + if not self.found(): + return mlog.red('NO') + return self.path + + def __repr__(self) -> str: + r = '<{} {!r} -> {!r}>' + return r.format(self.__class__.__name__, self.name, self.command) + + def description(self) -> str: + '''Human friendly description of the command''' + return ' '.join(self.command) + + @classmethod + def from_bin_list(cls, env: 'Environment', for_machine: MachineChoice, name): + # There is a static `for_machine` for this class because the binary + # aways runs on the build platform. (It's host platform is our build + # platform.) But some external programs have a target platform, so this + # is what we are specifying here. + command = env.lookup_binary_entry(for_machine, name) + if command is None: + return NonExistingExternalProgram() + return cls.from_entry(name, command) + + @staticmethod + @functools.lru_cache(maxsize=None) + def _windows_sanitize_path(path: str) -> str: + # Ensure that we use USERPROFILE even when inside MSYS, MSYS2, Cygwin, etc. + if 'USERPROFILE' not in os.environ: + return path + # The WindowsApps directory is a bit of a problem. It contains + # some zero-sized .exe files which have "reparse points", that + # might either launch an installed application, or might open + # a page in the Windows Store to download the application. + # + # To handle the case where the python interpreter we're + # running on came from the Windows Store, if we see the + # WindowsApps path in the search path, replace it with + # dirname(sys.executable). + appstore_dir = Path(os.environ['USERPROFILE']) / 'AppData' / 'Local' / 'Microsoft' / 'WindowsApps' + paths = [] + for each in path.split(os.pathsep): + if Path(each) != appstore_dir: + paths.append(each) + elif 'WindowsApps' in sys.executable: + paths.append(os.path.dirname(sys.executable)) + return os.pathsep.join(paths) + + @staticmethod + def from_entry(name, command): + if isinstance(command, list): + if len(command) == 1: + command = command[0] + # We cannot do any searching if the command is a list, and we don't + # need to search if the path is an absolute path. + if isinstance(command, list) or os.path.isabs(command): + return ExternalProgram(name, command=command, silent=True) + assert isinstance(command, str) + # Search for the command using the specified string! + return ExternalProgram(command, silent=True) + + @staticmethod + def _shebang_to_cmd(script: str) -> T.Optional[list]: + """ + 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: + first_line = f.readline().strip() + if first_line.startswith('#!'): + # In a shebang, everything before the first space is assumed to + # be the command to run and everything after the first space is + # the single argument to pass to that command. So we must split + # exactly once. + commands = first_line[2:].split('#')[0].strip().split(maxsplit=1) + if mesonlib.is_windows(): + # Windows does not have UNIX paths so remove them, + # but don't remove Windows paths + if commands[0].startswith('/'): + commands[0] = commands[0].split('/')[-1] + if len(commands) > 0 and commands[0] == 'env': + commands = commands[1:] + # Windows does not ship python3.exe, but we know the path to it + if len(commands) > 0 and commands[0] == 'python3': + commands = mesonlib.python_command + commands[1:] + elif mesonlib.is_haiku(): + # Haiku does not have /usr, but a lot of scripts assume that + # /usr/bin/env always exists. Detect that case and run the + # script with the interpreter after it. + if commands[0] == '/usr/bin/env': + commands = commands[1:] + # We know what python3 is, we're running on it + if len(commands) > 0 and commands[0] == 'python3': + commands = mesonlib.python_command + commands[1:] + else: + # Replace python3 with the actual python3 that we are using + if commands[0] == '/usr/bin/env' and commands[1] == 'python3': + commands = mesonlib.python_command + commands[2:] + elif commands[0].split('/')[-1] == 'python3': + commands = mesonlib.python_command + commands[1:] + return commands + [script] + except Exception as e: + mlog.debug(e) + mlog.debug(f'Unusable script {script!r}') + return None + + def _is_executable(self, path): + suffix = os.path.splitext(path)[-1].lower()[1:] + execmask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + if mesonlib.is_windows(): + if suffix in self.windows_exts: + return True + elif os.stat(path).st_mode & execmask: + return not os.path.isdir(path) + return False + + def _search_dir(self, name: str, search_dir: T.Optional[str]) -> T.Optional[list]: + if search_dir is None: + return None + trial = os.path.join(search_dir, name) + 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: + if mesonlib.is_windows(): + for ext in self.windows_exts: + trial_ext = f'{trial}.{ext}' + if os.path.exists(trial_ext): + return [trial_ext] + return None + + def _search_windows_special_cases(self, name: str, command: str) -> list: + ''' + Lots of weird Windows quirks: + 1. PATH search for @name returns files with extensions from PATHEXT, + but only self.windows_exts are executable without an interpreter. + 2. @name might be an absolute path to an executable, but without the + extension. This works inside MinGW so people use it a lot. + 3. The script is specified without an extension, in which case we have + to manually search in PATH. + 4. More special-casing for the shebang inside the script. + ''' + if command: + # 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(command)[1] + if name_ext[1:].lower() in self.windows_exts: + # Good, it can be directly executed + return [command] + # Try to extract the interpreter from the shebang + commands = self._shebang_to_cmd(command) + if commands: + return commands + return [None] + # 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: + command = f'{name}.{ext}' + if os.path.exists(command): + return [command] + # 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 = self._windows_sanitize_path(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 _search(self, name: str, search_dir: T.Optional[str]) -> list: + ''' + 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 + path = os.environ.get('PATH', None) + if mesonlib.is_windows() and path: + path = self._windows_sanitize_path(path) + command = shutil.which(name, path=path) + if mesonlib.is_windows(): + return self._search_windows_special_cases(name, command) + # On UNIX-like platforms, shutil.which() is enough to find + # all executables whether in PATH or with an absolute path + return [command] + + def found(self) -> bool: + return self.command[0] is not None + + def get_command(self) -> T.List[str]: + return self.command[:] + + def get_path(self) -> str: + return self.path + + def get_name(self) -> str: + return self.name + + +class NonExistingExternalProgram(ExternalProgram): # lgtm [py/missing-call-to-init] + "A program that will never exist" + + def __init__(self, name: str = 'nonexistingprogram') -> None: + self.name = name + self.command = [None] + self.path = None + + def __repr__(self) -> str: + r = '<{} {!r} -> {!r}>' + return r.format(self.__class__.__name__, self.name, self.command) + + def found(self) -> bool: + return False + + +class EmptyExternalProgram(ExternalProgram): # lgtm [py/missing-call-to-init] + ''' + A program object that returns an empty list of commands. Used for cases + such as a cross file exe_wrapper to represent that it's not required. + ''' + + def __init__(self): + self.name = None + self.command = [] + self.path = None + + def __repr__(self): + r = '<{} {!r} -> {!r}>' + return r.format(self.__class__.__name__, self.name, self.command) + + def found(self): + return True + + +def find_external_program(env: 'Environment', for_machine: MachineChoice, name: str, + display_name: str, default_names: T.List[str], + allow_default_for_cross: bool = True) -> T.Generator['ExternalProgram', None, None]: + """Find an external program, chcking the cross file plus any default options.""" + # Lookup in cross or machine file. + potential_cmd = env.lookup_binary_entry(for_machine, name) + if potential_cmd is not None: + mlog.debug(f'{display_name} binary for {for_machine} specified from cross file, native file, ' + f'or env var as {potential_cmd}') + yield ExternalProgram.from_entry(name, potential_cmd) + # We never fallback if the user-specified option is no good, so + # stop returning options. + return + mlog.debug(f'{display_name} binary missing from cross or native file, or env var undefined.') + # Fallback on hard-coded defaults, if a default binary is allowed for use + # with cross targets, or if this is not a cross target + if allow_default_for_cross or not (for_machine is MachineChoice.HOST and env.is_cross_build(for_machine)): + for potential_path in default_names: + mlog.debug(f'Trying a default {display_name} fallback at', potential_path) + yield ExternalProgram(potential_path, silent=True) + else: + mlog.debug('Default target is not allowed for cross use') diff --git a/run_unittests.py b/run_unittests.py index ef4a786..c22f884 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -61,7 +61,8 @@ from mesonbuild.mesonlib import ( ) from mesonbuild.environment import detect_ninja from mesonbuild.mesonlib import MesonException, EnvironmentException, OptionKey -from mesonbuild.dependencies import PkgConfigDependency, ExternalProgram +from mesonbuild.dependencies import PkgConfigDependency +from mesonbuild.programs import ExternalProgram import mesonbuild.dependencies.base from mesonbuild.build import Target, ConfigurationData import mesonbuild.modules.pkgconfig |