aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mesonbuild/backend/backends.py15
-rw-r--r--mesonbuild/build.py7
-rw-r--r--mesonbuild/cmake/executor.py9
-rw-r--r--mesonbuild/cmake/interpreter.py2
-rw-r--r--mesonbuild/compilers/c.py3
-rw-r--r--mesonbuild/compilers/cpp.py3
-rw-r--r--mesonbuild/compilers/cuda.py3
-rw-r--r--mesonbuild/compilers/d.py2
-rw-r--r--mesonbuild/compilers/fortran.py3
-rw-r--r--mesonbuild/compilers/mixins/clike.py3
-rw-r--r--mesonbuild/compilers/objc.py2
-rw-r--r--mesonbuild/compilers/objcpp.py2
-rw-r--r--mesonbuild/compilers/rust.py2
-rw-r--r--mesonbuild/dependencies/__init__.py9
-rw-r--r--mesonbuild/dependencies/base.py324
-rw-r--r--mesonbuild/dependencies/ui.py6
-rw-r--r--mesonbuild/environment.py6
-rw-r--r--mesonbuild/interpreter.py34
-rw-r--r--mesonbuild/modules/cmake.py5
-rw-r--r--mesonbuild/modules/dlang.py6
-rw-r--r--mesonbuild/modules/gnome.py3
-rw-r--r--mesonbuild/modules/hotdoc.py3
-rw-r--r--mesonbuild/modules/python.py4
-rw-r--r--mesonbuild/modules/python3.py7
-rw-r--r--mesonbuild/modules/qt.py3
-rw-r--r--mesonbuild/modules/unstable_rust.py2
-rw-r--r--mesonbuild/modules/windows.py2
-rw-r--r--mesonbuild/mtest.py2
-rw-r--r--mesonbuild/programs.py351
-rwxr-xr-xrun_unittests.py3
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