summaryrefslogtreecommitdiffstats
path: root/mesonbuild/programs.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/programs.py')
-rw-r--r--mesonbuild/programs.py375
1 files changed, 375 insertions, 0 deletions
diff --git a/mesonbuild/programs.py b/mesonbuild/programs.py
new file mode 100644
index 0000000..64f7c29
--- /dev/null
+++ b/mesonbuild/programs.py
@@ -0,0 +1,375 @@
+# 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.
+from __future__ import annotations
+
+"""Representations and logic for External and Internal Programs."""
+
+import functools
+import os
+import shutil
+import stat
+import sys
+import re
+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
+ from .interpreter import Interpreter
+
+
+class ExternalProgram(mesonlib.HoldableObject):
+
+ """A program that is found on the system."""
+
+ windows_exts = ('exe', 'msc', 'com', 'bat', 'cmd')
+ 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
+ self.path: T.Optional[str] = None
+ self.cached_version: T.Optional[str] = None
+ 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
+
+ if self.found():
+ # 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.
+ for arg in reversed(self.command):
+ if arg is not None and os.path.isfile(arg):
+ self.path = arg
+ break
+ else:
+ self.path = self.command[-1]
+
+ 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)
+
+ def get_version(self, interpreter: T.Optional['Interpreter'] = None) -> str:
+ if not self.cached_version:
+ from . import build
+ raw_cmd = self.get_command() + ['--version']
+ if interpreter:
+ res = interpreter.run_command_impl(interpreter.current_node, (self, ['--version']),
+ {'capture': True,
+ 'check': True,
+ 'env': build.EnvironmentVariables()},
+ True)
+ o, e = res.stdout, res.stderr
+ else:
+ p, o, e = mesonlib.Popen_safe(raw_cmd)
+ if p.returncode != 0:
+ cmd_str = mesonlib.join_args(raw_cmd)
+ raise mesonlib.MesonException(f'Command {cmd_str!r} failed with status {p.returncode}.')
+ output = o.strip()
+ if not output:
+ output = e.strip()
+ match = re.search(r'([0-9][0-9\.]+)', output)
+ if not match:
+ raise mesonlib.MesonException(f'Could not find a version number in output of {raw_cmd!r}')
+ self.cached_version = match.group(1)
+ return self.cached_version
+
+ @classmethod
+ def from_bin_list(cls, env: 'Environment', for_machine: MachineChoice, name: str) -> 'ExternalProgram':
+ # There is a static `for_machine` for this class because the binary
+ # always 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: str, command: T.Union[str, T.List[str]]) -> 'ExternalProgram':
+ 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):
+ if isinstance(command, str):
+ command = [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[T.List[str]]:
+ """
+ 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, encoding='utf-8') 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(str(e))
+ mlog.debug(f'Unusable script {script!r}')
+ return None
+
+ def _is_executable(self, path: str) -> bool:
+ 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) -> T.List[T.Optional[str]]:
+ '''
+ 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]) -> T.List[T.Optional[str]]:
+ '''
+ 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
+ # If there is a directory component, do not look in PATH
+ if os.path.dirname(name) and not os.path.isabs(name):
+ return [None]
+ # 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) -> T.Optional[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 OverrideProgram(ExternalProgram):
+
+ """A script overriding a program."""
+
+
+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')