diff options
Diffstat (limited to 'mesonbuild/dependencies/base.py')
-rw-r--r-- | mesonbuild/dependencies/base.py | 324 |
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) |