aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/dependencies/base.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/dependencies/base.py')
-rw-r--r--mesonbuild/dependencies/base.py324
1 files changed, 1 insertions, 323 deletions
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)