diff options
Diffstat (limited to 'mesonbuild/modules/python.py')
-rw-r--r-- | mesonbuild/modules/python.py | 473 |
1 files changed, 257 insertions, 216 deletions
diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index 46fd27b..450991b 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -12,41 +12,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +from pathlib import Path +import functools import json +import os import shutil import typing as T -from pathlib import Path -from .. import mesonlib -from ..mesonlib import MachineChoice, MesonException from . import ExtensionModule +from .. import mesonlib +from .. import mlog +from ..coredata import UserFeatureOption +from ..build import known_shmod_kwargs +from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency, ExtraFrameworkDependency +from ..dependencies.base import process_method_kw +from ..environment import detect_cpu_family +from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs from ..interpreterbase import ( - noPosargs, noKwargs, permittedKwargs, - InvalidArguments, + noPosargs, noKwargs, permittedKwargs, ContainerTypeInfo, + InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo, FeatureNew, FeatureNewKwargs, disablerIfNotFound ) -from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs -from ..build import known_shmod_kwargs -from .. import mlog -from ..environment import detect_cpu_family -from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency +from ..mesonlib import MachineChoice from ..programs import ExternalProgram, NonExistingExternalProgram +if T.TYPE_CHECKING: + from . import ModuleState + from ..build import SharedModule, Data + from ..dependencies import ExternalDependency, Dependency + from ..dependencies.factory import DependencyGenerator + from ..environment import Environment + from ..interpreter import Interpreter + from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs + + from typing_extensions import TypedDict + + mod_kwargs = {'subdir'} mod_kwargs.update(known_shmod_kwargs) mod_kwargs -= {'name_prefix', 'name_suffix'} -class PythonDependency(SystemDependency): - def __init__(self, python_holder, environment, kwargs): - super().__init__('python', environment, kwargs) - self.name = 'python' - self.static = kwargs.get('static', False) - self.embed = kwargs.get('embed', False) - self.version = python_holder.version +if T.TYPE_CHECKING: + _Base = ExternalDependency +else: + _Base = object + +class _PythonDependencyBase(_Base): + + def __init__(self, python_holder: 'PythonInstallation', embed: bool): + self.name = 'python' # override the name from the "real" dependency lookup + self.embed = embed + self.version: str = python_holder.version self.platform = python_holder.platform - self.pkgdep = None self.variables = python_holder.variables self.paths = python_holder.paths self.link_libpython = python_holder.link_libpython @@ -56,75 +74,36 @@ class PythonDependency(SystemDependency): else: self.major_version = 2 - # We first try to find the necessary python variables using pkgconfig - if DependencyMethods.PKGCONFIG in self.methods and not python_holder.is_pypy: - pkg_version = self.variables.get('LDVERSION') or self.version - pkg_libdir = self.variables.get('LIBPC') - pkg_embed = '-embed' if self.embed and mesonlib.version_compare(self.version, '>=3.8') else '' - pkg_name = f'python-{pkg_version}{pkg_embed}' - # If python-X.Y.pc exists in LIBPC, we will try to use it - if pkg_libdir is not None and Path(os.path.join(pkg_libdir, f'{pkg_name}.pc')).is_file(): - old_pkg_libdir = os.environ.get('PKG_CONFIG_LIBDIR') - old_pkg_path = os.environ.get('PKG_CONFIG_PATH') +class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase): - os.environ.pop('PKG_CONFIG_PATH', None) + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): + PkgConfigDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) - if pkg_libdir: - os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir - try: - self.pkgdep = PkgConfigDependency(pkg_name, environment, kwargs) - mlog.debug(f'Found "{pkg_name}" via pkgconfig lookup in LIBPC ({pkg_libdir})') - py_lookup_method = 'pkgconfig' - except MesonException as e: - mlog.debug(f'"{pkg_name}" could not be found in LIBPC ({pkg_libdir})') - mlog.debug(e) +class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase): - if old_pkg_path is not None: - os.environ['PKG_CONFIG_PATH'] = old_pkg_path + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): + ExtraFrameworkDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) - if old_pkg_libdir is not None: - os.environ['PKG_CONFIG_LIBDIR'] = old_pkg_libdir - else: - os.environ.pop('PKG_CONFIG_LIBDIR', None) - else: - mlog.debug(f'"{pkg_name}" could not be found in LIBPC ({pkg_libdir}), this is likely due to a relocated python installation') - - # If lookup via LIBPC failed, try to use fallback PKG_CONFIG_LIBDIR/PKG_CONFIG_PATH mechanisms - if self.pkgdep is None or not self.pkgdep.found(): - try: - self.pkgdep = PkgConfigDependency(pkg_name, environment, kwargs) - mlog.debug(f'Found "{pkg_name}" via fallback pkgconfig lookup in PKG_CONFIG_LIBDIR/PKG_CONFIG_PATH') - py_lookup_method = 'pkgconfig-fallback' - except MesonException as e: - mlog.debug(f'"{pkg_name}" could not be found via fallback pkgconfig lookup in PKG_CONFIG_LIBDIR/PKG_CONFIG_PATH') - mlog.debug(e) - - if self.pkgdep and self.pkgdep.found(): - self.compile_args = self.pkgdep.get_compile_args() - self.link_args = self.pkgdep.get_link_args() - self.is_found = True - self.pcdep = self.pkgdep - else: - self.pkgdep = None - # Finally, try to find python via SYSCONFIG as a final measure - if DependencyMethods.SYSCONFIG in self.methods: - if mesonlib.is_windows(): - self._find_libpy_windows(environment) - else: - self._find_libpy(python_holder, environment) - if self.is_found: - mlog.debug(f'Found "python-{self.version}" via SYSCONFIG module') - py_lookup_method = 'sysconfig' +class PythonSystemDependency(SystemDependency, _PythonDependencyBase): + + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): + SystemDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) - if self.is_found: - mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.green(f'YES ({py_lookup_method})')) + if mesonlib.is_windows(): + self._find_libpy_windows(environment) else: - mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.red('NO')) + self._find_libpy(installation, environment) - def _find_libpy(self, python_holder, environment): + def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None: if python_holder.is_pypy: if self.major_version == 3: libname = 'pypy3-c' @@ -153,7 +132,7 @@ class PythonDependency(SystemDependency): self.compile_args += ['-I' + path for path in inc_paths if path] - def get_windows_python_arch(self): + def _get_windows_python_arch(self) -> T.Optional[str]: if self.platform == 'mingw': pycc = self.variables.get('CC') if pycc.startswith('x86_64'): @@ -171,7 +150,7 @@ class PythonDependency(SystemDependency): mlog.log(f'Unknown Windows Python platform {self.platform!r}') return None - def get_windows_link_args(self): + def _get_windows_link_args(self) -> T.Optional[T.List[str]]: if self.platform.startswith('win'): vernum = self.variables.get('py_version_nodot') if self.static: @@ -179,7 +158,7 @@ class PythonDependency(SystemDependency): else: comp = self.get_compiler() if comp.id == "gcc": - libpath = f'python{vernum}.dll' + libpath = Path(f'python{vernum}.dll') else: libpath = Path('libs') / f'python{vernum}.lib' lib = Path(self.variables.get('base')) / libpath @@ -189,17 +168,20 @@ class PythonDependency(SystemDependency): else: libname = self.variables.get('LDLIBRARY') lib = Path(self.variables.get('LIBDIR')) / libname + else: + raise mesonlib.MesonBugException( + 'On a Windows path, but the OS doesn\'t appear to be Windows or MinGW.') if not lib.exists(): mlog.log('Could not find Python3 library {!r}'.format(str(lib))) return None return [str(lib)] - def _find_libpy_windows(self, env): + def _find_libpy_windows(self, env: 'Environment') -> None: ''' Find python3 libraries on Windows and also verify that the arch matches what we are building for. ''' - pyarch = self.get_windows_python_arch() + pyarch = self._get_windows_python_arch() if pyarch is None: self.is_found = False return @@ -221,7 +203,7 @@ class PythonDependency(SystemDependency): self.is_found = False return # This can fail if the library is not found - largs = self.get_windows_link_args() + largs = self._get_windows_link_args() if largs is None: self.is_found = False return @@ -240,23 +222,57 @@ class PythonDependency(SystemDependency): self.is_found = True - @staticmethod - def get_methods(): - if mesonlib.is_windows(): - return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSCONFIG] - elif mesonlib.is_osx(): - return [DependencyMethods.PKGCONFIG, DependencyMethods.EXTRAFRAMEWORK] - else: - return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSCONFIG] - def get_pkgconfig_variable(self, variable_name, kwargs): - if self.pkgdep: - return self.pkgdep.get_pkgconfig_variable(variable_name, kwargs) - else: - return super().get_pkgconfig_variable(variable_name, kwargs) +def python_factory(env: 'Environment', for_machine: 'MachineChoice', + kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods], + installation: 'PythonInstallation') -> T.List['DependencyGenerator']: + # We can't use the factory_methods decorator here, as we need to pass the + # extra installation argument + embed = kwargs.get('embed', False) + candidates: T.List['DependencyGenerator'] = [] + pkg_version = installation.variables.get('LDVERSION') or installation.version + + if DependencyMethods.PKGCONFIG in methods: + pkg_libdir = installation.variables.get('LIBPC') + pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.version, '>=3.8') else '' + pkg_name = f'python-{pkg_version}{pkg_embed}' + + # If python-X.Y.pc exists in LIBPC, we will try to use it + def wrap_in_pythons_pc_dir(name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], + installation: 'PythonInstallation') -> 'ExternalDependency': + old_pkg_libdir = os.environ.pop('PKG_CONFIG_LIBDIR', None) + old_pkg_path = os.environ.pop('PKG_CONFIG_PATH', None) + if pkg_libdir: + os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir + try: + return PythonPkgConfigDependency(name, env, kwargs, installation) + finally: + if old_pkg_libdir is not None: + os.environ['PKG_CONFIG_LIBDIR'] = old_pkg_libdir + if old_pkg_path is not None: + os.environ['PKG_CONFIG_PATH'] = old_pkg_path + candidates.extend([ + functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation), + functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation) + ]) -INTROSPECT_COMMAND = '''import sysconfig + if DependencyMethods.SYSTEM in methods: + candidates.append(functools.partial(PythonSystemDependency, 'python', env, kwargs, installation)) + + if DependencyMethods.EXTRAFRAMEWORK in methods: + nkwargs = kwargs.copy() + if mesonlib.version_compare(pkg_version, '>= 3'): + # There is a python in /System/Library/Frameworks, but thats python 2.x, + # Python 3 will always be in /Library + nkwargs['paths'] = ['/Library/Frameworks'] + candidates.append(functools.partial(PythonFrameworkDependency, 'Python', env, nkwargs, installation)) + + return candidates + + +INTROSPECT_COMMAND = '''\ +import sysconfig import json import sys @@ -268,7 +284,7 @@ def links_against_libpython(): cmd.ensure_finalized() return bool(cmd.get_libraries(Extension('dummy', []))) -print (json.dumps ({ +print(json.dumps({ 'variables': sysconfig.get_config_vars(), 'paths': sysconfig.get_paths(), 'install_paths': install_paths, @@ -279,21 +295,60 @@ print (json.dumps ({ })) ''' +if T.TYPE_CHECKING: + class PythonIntrospectionDict(TypedDict): + + install_paths: T.Dict[str, str] + is_pypy: bool + link_libpython: bool + paths: T.Dict[str, str] + platform: str + variables: T.Dict[str, str] + version: str + + class PythonExternalProgram(ExternalProgram): - def __init__(self, name: str, command: T.Optional[T.List[str]] = None, ext_prog: T.Optional[ExternalProgram] = None): + def __init__(self, name: str, command: T.Optional[T.List[str]] = None, + ext_prog: T.Optional[ExternalProgram] = None): if ext_prog is None: super().__init__(name, command=command, silent=True) else: self.name = ext_prog.name self.command = ext_prog.command self.path = ext_prog.path - self.info: T.Dict[str, str] = {} + + # We want strong key values, so we always populate this with bogus data. + # Otherwise to make the type checkers happy we'd have to do .get() for + # everycall, even though we konw that the introspection data will be + # complete + self.info: 'PythonIntrospectionDict' = { + 'install_paths': {}, + 'is_pypy': False, + 'link_libpython': False, + 'paths': {}, + 'platform': 'sentinal', + 'variables': {}, + 'version': '0.0', + } + + +_PURE_KW = KwargInfo('pure', bool, default=True) +_SUBDIR_KW = KwargInfo('subdir', str, default='') + +if T.TYPE_CHECKING: + + class PyInstallKw(TypedDict): + + pure: bool + subdir: str + class PythonInstallation(ExternalProgramHolder): - def __init__(self, python, interpreter): + def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'): ExternalProgramHolder.__init__(self, python, interpreter) info = python.info prefix = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('prefix')) + assert isinstance(prefix, str), 'for mypy' self.variables = info['variables'] self.paths = info['paths'] install_paths = info['install_paths'] @@ -318,7 +373,7 @@ class PythonInstallation(ExternalProgramHolder): }) @permittedKwargs(mod_kwargs) - def extension_module_method(self, args, kwargs): + def extension_module_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'SharedModule': if 'subdir' in kwargs and 'install_dir' in kwargs: raise InvalidArguments('"subdir" and "install_dir" are mutually exclusive') @@ -335,7 +390,7 @@ class PythonInstallation(ExternalProgramHolder): if not self.link_libpython: new_deps = [] for dep in mesonlib.extract_as_list(kwargs, 'dependencies'): - if isinstance(dep, PythonDependency): + if isinstance(dep, _PythonDependencyBase): dep = dep.get_partial_dependency(compile_args=True) new_deps.append(dep) kwargs['dependencies'] = new_deps @@ -354,136 +409,113 @@ class PythonInstallation(ExternalProgramHolder): @permittedKwargs(permitted_dependency_kwargs | {'embed'}) @FeatureNewKwargs('python_installation.dependency', '0.53.0', ['embed']) - def dependency_method(self, args, kwargs): - if args: - mlog.warning('python_installation.dependency() does not take any ' - 'positional arguments. It always returns a Python ' - 'dependency. This will become an error in the future.', - location=self.interpreter.current_node) + @noPosargs + def dependency_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'Dependency': disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) + + # it's theoretically (though not practically) possible for the else clse + # to not bind dep, let's ensure it is. + dep: 'Dependency' = NotFoundDependency(self.interpreter.environment) if disabled: mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled') - dep = NotFoundDependency(self.interpreter.environment) else: - dep = PythonDependency(self, self.interpreter.environment, kwargs) + for d in python_factory(self.interpreter.environment, + MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST, + kwargs, + process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs), + self): + dep = d() + if dep.found(): + break if required and not dep.found(): raise mesonlib.MesonException('Python dependency not found') - return dep - - @permittedKwargs(['pure', 'subdir']) - def install_sources_method(self, args, kwargs): - pure = kwargs.pop('pure', True) - if not isinstance(pure, bool): - raise InvalidArguments('"pure" argument must be a boolean.') - subdir = kwargs.pop('subdir', '') - if not isinstance(subdir, str): - raise InvalidArguments('"subdir" argument must be a string.') - - if pure: - kwargs['install_dir'] = os.path.join(self.purelib_install_path, subdir) - else: - kwargs['install_dir'] = os.path.join(self.platlib_install_path, subdir) + return dep - return self.interpreter.func_install_data(None, args, kwargs) + @typed_pos_args('install_data', varargs=(str, mesonlib.File)) + @typed_kwargs('python_installation.install_sources', _PURE_KW, _SUBDIR_KW) + def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]], + kwargs: 'PyInstallKw') -> 'Data': + return self.interpreter.install_data_impl( + self.interpreter.source_strings_to_files(args[0]), + self._get_install_dir_impl(kwargs['pure'], kwargs['subdir']), + mesonlib.FileMode(), + None) @noPosargs - @permittedKwargs(['pure', 'subdir']) - def get_install_dir_method(self, args, kwargs): - pure = kwargs.pop('pure', True) - if not isinstance(pure, bool): - raise InvalidArguments('"pure" argument must be a boolean.') - - subdir = kwargs.pop('subdir', '') - if not isinstance(subdir, str): - raise InvalidArguments('"subdir" argument must be a string.') - - if pure: - res = os.path.join(self.purelib_install_path, subdir) - else: - res = os.path.join(self.platlib_install_path, subdir) + @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) + def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str: + return self._get_install_dir_impl(kwargs['pure'], kwargs['subdir']) - return res + def _get_install_dir_impl(self, pure: bool, subdir: str) -> str: + return os.path.join( + self.purelib_install_path if pure else self.platlib_install_path, subdir) @noPosargs @noKwargs - def language_version_method(self, args, kwargs): + def language_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: return self.version + @typed_pos_args('python_installation.has_path', str) @noKwargs - def has_path_method(self, args, kwargs): - if len(args) != 1: - raise InvalidArguments('has_path takes exactly one positional argument.') - path_name = args[0] - if not isinstance(path_name, str): - raise InvalidArguments('has_path argument must be a string.') - - return path_name in self.paths + def has_path_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: + return args[0] in self.paths + @typed_pos_args('python_installation.get_path', str, optargs=[object]) @noKwargs - def get_path_method(self, args, kwargs): - if len(args) not in (1, 2): - raise InvalidArguments('get_path must have one or two arguments.') - path_name = args[0] - if not isinstance(path_name, str): - raise InvalidArguments('get_path argument must be a string.') - + def get_path_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': + path_name, fallback = args try: - path = self.paths[path_name] + return self.paths[path_name] except KeyError: - if len(args) == 2: - path = args[1] - else: - raise InvalidArguments(f'{path_name} is not a valid path name') - - return path + if fallback is not None: + return fallback + raise InvalidArguments(f'{path_name} is not a valid path name') + @typed_pos_args('python_installation.has_variable', str) @noKwargs - def has_variable_method(self, args, kwargs): - if len(args) != 1: - raise InvalidArguments('has_variable takes exactly one positional argument.') - var_name = args[0] - if not isinstance(var_name, str): - raise InvalidArguments('has_variable argument must be a string.') - - return var_name in self.variables + def has_variable_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: + return args[0] in self.variables + @typed_pos_args('python_installation.get_variable', str, optargs=[object]) @noKwargs - def get_variable_method(self, args, kwargs): - if len(args) not in (1, 2): - raise InvalidArguments('get_variable must have one or two arguments.') - var_name = args[0] - if not isinstance(var_name, str): - raise InvalidArguments('get_variable argument must be a string.') - + def get_variable_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': + var_name, fallback = args try: - var = self.variables[var_name] + return self.variables[var_name] except KeyError: - if len(args) == 2: - var = args[1] - else: - raise InvalidArguments(f'{var_name} is not a valid variable name') - - return var + if fallback is not None: + return fallback + raise InvalidArguments(f'{var_name} is not a valid variable name') @noPosargs @noKwargs @FeatureNew('Python module path method', '0.50.0') - def path_method(self, args, kwargs): + def path_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: return super().path_method(args, kwargs) +if T.TYPE_CHECKING: + from ..interpreter.kwargs import ExtractRequired + + class FindInstallationKw(ExtractRequired): + + disabler: bool + modules: T.List[str] + + class PythonModule(ExtensionModule): @FeatureNew('Python Module', '0.46.0') - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, interpreter: 'Interpreter') -> None: + super().__init__(interpreter) self.methods.update({ 'find_installation': self.find_installation, }) # https://www.python.org/dev/peps/pep-0397/ - def _get_win_pythonpath(self, name_or_path): + @staticmethod + def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]: if name_or_path not in ['python2', 'python3']: return None if not shutil.which('py'): @@ -498,32 +530,41 @@ class PythonModule(ExtensionModule): else: return None - def _check_version(self, name_or_path, version): + @staticmethod + def _check_version(name_or_path: str, version: str) -> bool: if name_or_path == 'python2': return mesonlib.version_compare(version, '< 3.0') elif name_or_path == 'python3': return mesonlib.version_compare(version, '>= 3.0') return True - @FeatureNewKwargs('python.find_installation', '0.49.0', ['disabler']) - @FeatureNewKwargs('python.find_installation', '0.51.0', ['modules']) @disablerIfNotFound - @permittedKwargs({'required', 'modules'}) - def find_installation(self, state, args, kwargs): + @typed_pos_args('python.find_installation', optargs=[str]) + @typed_kwargs( + 'python.find_installation', + KwargInfo('required', (bool, UserFeatureOption), default=True), + KwargInfo('disabler', bool, default=False, since='0.49.0'), + KwargInfo('modules', ContainerTypeInfo(list, str), listify=True, default=[], since='0.51.0'), + ) + def find_installation(self, state: 'ModuleState', args: T.Tuple[T.Optional[str]], + kwargs: 'FindInstallationKw') -> ExternalProgram: feature_check = FeatureNew('Passing "feature" option to find_installation', '0.48.0') disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, feature_check) - want_modules = mesonlib.extract_as_list(kwargs, 'modules') # type: T.List[str] - found_modules = [] # type: T.List[str] - missing_modules = [] # type: T.List[str] - - if len(args) > 1: - raise InvalidArguments('find_installation takes zero or one positional argument.') - - name_or_path = state.environment.lookup_binary_entry(MachineChoice.HOST, 'python') - if name_or_path is None and args: - name_or_path = args[0] - if not isinstance(name_or_path, str): - raise InvalidArguments('find_installation argument must be a string.') + want_modules = kwargs['modules'] + found_modules: T.List[str] = [] + missing_modules: T.List[str] = [] + + # FIXME: this code is *full* of sharp corners. It assumes that it's + # going to get a string value (or now a list of lenght 1), of `python2` + # or `python3` which is completely nonsense. On windows the value could + # easily be `['py', '-3']`, or `['py', '-3.7']` to get a very specific + # version of python. On Linux we might want a python that's not in + # $PATH, or that uses a wrapper of some kind. + np: T.List[str] = state.environment.lookup_binary_entry(MachineChoice.HOST, 'python') or [] + fallback = args[0] + if not np and fallback is not None: + np = [fallback] + name_or_path = np[0] if np else None if disabled: mlog.log('Program', name_or_path or 'python', 'found:', mlog.red('NO'), '(disabled by:', mlog.bold(feature), ')') @@ -550,7 +591,7 @@ class PythonModule(ExtensionModule): if python.found() and want_modules: for mod in want_modules: - p, out, err = mesonlib.Popen_safe( + p, *_ = mesonlib.Popen_safe( python.command + ['-c', f'import {mod}']) if p.returncode != 0: @@ -558,7 +599,7 @@ class PythonModule(ExtensionModule): else: found_modules.append(mod) - msg = ['Program', python.name] + msg: T.List['mlog.TV_Loggable'] = ['Program', python.name] if want_modules: msg.append('({})'.format(', '.join(want_modules))) msg.append('found:') @@ -582,9 +623,9 @@ class PythonModule(ExtensionModule): return NonExistingExternalProgram() else: # Sanity check, we expect to have something that at least quacks in tune + cmd = python.get_command() + ['-c', INTROSPECT_COMMAND] + p, stdout, stderr = mesonlib.Popen_safe(cmd) try: - cmd = python.get_command() + ['-c', INTROSPECT_COMMAND] - p, stdout, stderr = mesonlib.Popen_safe(cmd) info = json.loads(stdout) except json.JSONDecodeError: info = None @@ -595,7 +636,7 @@ class PythonModule(ExtensionModule): mlog.debug(stderr) if isinstance(info, dict) and 'version' in info and self._check_version(name_or_path, info['version']): - python.info = info + python.info = T.cast('PythonIntrospectionDict', info) return python else: if required: @@ -605,7 +646,7 @@ class PythonModule(ExtensionModule): raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).') -def initialize(*args, **kwargs): - mod = PythonModule(*args, **kwargs) +def initialize(interpreter: 'Interpreter') -> PythonModule: + mod = PythonModule(interpreter) mod.interpreter.append_holder_map(PythonExternalProgram, PythonInstallation) return mod |