1# Copyright 2018 The Meson development team 2 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6 7# http://www.apache.org/licenses/LICENSE-2.0 8 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from pathlib import Path 16import functools 17import json 18import os 19import shutil 20import typing as T 21 22from . import ExtensionModule 23from .. import mesonlib 24from .. import mlog 25from ..coredata import UserFeatureOption 26from ..build import known_shmod_kwargs 27from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency, ExtraFrameworkDependency 28from ..dependencies.base import process_method_kw 29from ..environment import detect_cpu_family 30from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs 31from ..interpreter.type_checking import NoneType 32from ..interpreterbase import ( 33 noPosargs, noKwargs, permittedKwargs, ContainerTypeInfo, 34 InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo, 35 FeatureNew, FeatureNewKwargs, disablerIfNotFound 36) 37from ..mesonlib import MachineChoice 38from ..programs import ExternalProgram, NonExistingExternalProgram 39 40if T.TYPE_CHECKING: 41 from . import ModuleState 42 from ..build import SharedModule, Data 43 from ..dependencies import ExternalDependency, Dependency 44 from ..dependencies.factory import DependencyGenerator 45 from ..environment import Environment 46 from ..interpreter import Interpreter 47 from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs 48 49 from typing_extensions import TypedDict 50 51 52mod_kwargs = {'subdir'} 53mod_kwargs.update(known_shmod_kwargs) 54mod_kwargs -= {'name_prefix', 'name_suffix'} 55 56 57if T.TYPE_CHECKING: 58 _Base = ExternalDependency 59else: 60 _Base = object 61 62class _PythonDependencyBase(_Base): 63 64 def __init__(self, python_holder: 'PythonInstallation', embed: bool): 65 self.name = 'python' # override the name from the "real" dependency lookup 66 self.embed = embed 67 self.version: str = python_holder.version 68 self.platform = python_holder.platform 69 self.variables = python_holder.variables 70 self.paths = python_holder.paths 71 self.link_libpython = python_holder.link_libpython 72 self.info: T.Optional[T.Dict[str, str]] = None 73 if mesonlib.version_compare(self.version, '>= 3.0'): 74 self.major_version = 3 75 else: 76 self.major_version = 2 77 78 79class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase): 80 81 def __init__(self, name: str, environment: 'Environment', 82 kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): 83 PkgConfigDependency.__init__(self, name, environment, kwargs) 84 _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) 85 86 87class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase): 88 89 def __init__(self, name: str, environment: 'Environment', 90 kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): 91 ExtraFrameworkDependency.__init__(self, name, environment, kwargs) 92 _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) 93 94 95class PythonSystemDependency(SystemDependency, _PythonDependencyBase): 96 97 def __init__(self, name: str, environment: 'Environment', 98 kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): 99 SystemDependency.__init__(self, name, environment, kwargs) 100 _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) 101 102 if mesonlib.is_windows(): 103 self._find_libpy_windows(environment) 104 else: 105 self._find_libpy(installation, environment) 106 107 def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None: 108 if python_holder.is_pypy: 109 if self.major_version == 3: 110 libname = 'pypy3-c' 111 else: 112 libname = 'pypy-c' 113 libdir = os.path.join(self.variables.get('base'), 'bin') 114 libdirs = [libdir] 115 else: 116 libname = f'python{self.version}' 117 if 'DEBUG_EXT' in self.variables: 118 libname += self.variables['DEBUG_EXT'] 119 if 'ABIFLAGS' in self.variables: 120 libname += self.variables['ABIFLAGS'] 121 libdirs = [] 122 123 largs = self.clib_compiler.find_library(libname, environment, libdirs) 124 if largs is not None: 125 self.link_args = largs 126 127 self.is_found = largs is not None or self.link_libpython 128 129 inc_paths = mesonlib.OrderedSet([ 130 self.variables.get('INCLUDEPY'), 131 self.paths.get('include'), 132 self.paths.get('platinclude')]) 133 134 self.compile_args += ['-I' + path for path in inc_paths if path] 135 136 def _get_windows_python_arch(self) -> T.Optional[str]: 137 if self.platform == 'mingw': 138 pycc = self.variables.get('CC') 139 if pycc.startswith('x86_64'): 140 return '64' 141 elif pycc.startswith(('i686', 'i386')): 142 return '32' 143 else: 144 mlog.log(f'MinGW Python built with unknown CC {pycc!r}, please file a bug') 145 return None 146 elif self.platform == 'win32': 147 return '32' 148 elif self.platform in ('win64', 'win-amd64'): 149 return '64' 150 mlog.log(f'Unknown Windows Python platform {self.platform!r}') 151 return None 152 153 def _get_windows_link_args(self) -> T.Optional[T.List[str]]: 154 if self.platform.startswith('win'): 155 vernum = self.variables.get('py_version_nodot') 156 if self.static: 157 libpath = Path('libs') / f'libpython{vernum}.a' 158 else: 159 comp = self.get_compiler() 160 if comp.id == "gcc": 161 libpath = Path(f'python{vernum}.dll') 162 else: 163 libpath = Path('libs') / f'python{vernum}.lib' 164 lib = Path(self.variables.get('base')) / libpath 165 elif self.platform == 'mingw': 166 if self.static: 167 libname = self.variables.get('LIBRARY') 168 else: 169 libname = self.variables.get('LDLIBRARY') 170 lib = Path(self.variables.get('LIBDIR')) / libname 171 else: 172 raise mesonlib.MesonBugException( 173 'On a Windows path, but the OS doesn\'t appear to be Windows or MinGW.') 174 if not lib.exists(): 175 mlog.log('Could not find Python3 library {!r}'.format(str(lib))) 176 return None 177 return [str(lib)] 178 179 def _find_libpy_windows(self, env: 'Environment') -> None: 180 ''' 181 Find python3 libraries on Windows and also verify that the arch matches 182 what we are building for. 183 ''' 184 pyarch = self._get_windows_python_arch() 185 if pyarch is None: 186 self.is_found = False 187 return 188 arch = detect_cpu_family(env.coredata.compilers.host) 189 if arch == 'x86': 190 arch = '32' 191 elif arch == 'x86_64': 192 arch = '64' 193 else: 194 # We can't cross-compile Python 3 dependencies on Windows yet 195 mlog.log(f'Unknown architecture {arch!r} for', 196 mlog.bold(self.name)) 197 self.is_found = False 198 return 199 # Pyarch ends in '32' or '64' 200 if arch != pyarch: 201 mlog.log('Need', mlog.bold(self.name), f'for {arch}-bit, but found {pyarch}-bit') 202 self.is_found = False 203 return 204 # This can fail if the library is not found 205 largs = self._get_windows_link_args() 206 if largs is None: 207 self.is_found = False 208 return 209 self.link_args = largs 210 # Compile args 211 inc_paths = mesonlib.OrderedSet([ 212 self.variables.get('INCLUDEPY'), 213 self.paths.get('include'), 214 self.paths.get('platinclude')]) 215 216 self.compile_args += ['-I' + path for path in inc_paths if path] 217 218 # https://sourceforge.net/p/mingw-w64/mailman/message/30504611/ 219 if pyarch == '64' and self.major_version == 2: 220 self.compile_args += ['-DMS_WIN64'] 221 222 self.is_found = True 223 224 225def python_factory(env: 'Environment', for_machine: 'MachineChoice', 226 kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods], 227 installation: 'PythonInstallation') -> T.List['DependencyGenerator']: 228 # We can't use the factory_methods decorator here, as we need to pass the 229 # extra installation argument 230 embed = kwargs.get('embed', False) 231 candidates: T.List['DependencyGenerator'] = [] 232 pkg_version = installation.variables.get('LDVERSION') or installation.version 233 234 if DependencyMethods.PKGCONFIG in methods: 235 pkg_libdir = installation.variables.get('LIBPC') 236 pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.version, '>=3.8') else '' 237 pkg_name = f'python-{pkg_version}{pkg_embed}' 238 239 # If python-X.Y.pc exists in LIBPC, we will try to use it 240 def wrap_in_pythons_pc_dir(name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], 241 installation: 'PythonInstallation') -> 'ExternalDependency': 242 old_pkg_libdir = os.environ.pop('PKG_CONFIG_LIBDIR', None) 243 old_pkg_path = os.environ.pop('PKG_CONFIG_PATH', None) 244 if pkg_libdir: 245 os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir 246 try: 247 return PythonPkgConfigDependency(name, env, kwargs, installation) 248 finally: 249 def set_env(name, value): 250 if value is not None: 251 os.environ[name] = value 252 elif name in os.environ: 253 del os.environ[name] 254 set_env('PKG_CONFIG_LIBDIR', old_pkg_libdir) 255 set_env('PKG_CONFIG_PATH', old_pkg_path) 256 257 candidates.extend([ 258 functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation), 259 functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation) 260 ]) 261 262 if DependencyMethods.SYSTEM in methods: 263 candidates.append(functools.partial(PythonSystemDependency, 'python', env, kwargs, installation)) 264 265 if DependencyMethods.EXTRAFRAMEWORK in methods: 266 nkwargs = kwargs.copy() 267 if mesonlib.version_compare(pkg_version, '>= 3'): 268 # There is a python in /System/Library/Frameworks, but that's python 2.x, 269 # Python 3 will always be in /Library 270 nkwargs['paths'] = ['/Library/Frameworks'] 271 candidates.append(functools.partial(PythonFrameworkDependency, 'Python', env, nkwargs, installation)) 272 273 return candidates 274 275 276INTROSPECT_COMMAND = '''\ 277import os.path 278import sysconfig 279import json 280import sys 281import distutils.command.install 282 283def get_distutils_paths(scheme=None, prefix=None): 284 import distutils.dist 285 distribution = distutils.dist.Distribution() 286 install_cmd = distribution.get_command_obj('install') 287 if prefix is not None: 288 install_cmd.prefix = prefix 289 if scheme: 290 install_cmd.select_scheme(scheme) 291 install_cmd.finalize_options() 292 return { 293 'data': install_cmd.install_data, 294 'include': os.path.dirname(install_cmd.install_headers), 295 'platlib': install_cmd.install_platlib, 296 'purelib': install_cmd.install_purelib, 297 'scripts': install_cmd.install_scripts, 298 } 299 300# On Debian derivatives, the Python interpreter shipped by the distribution uses 301# a custom install scheme, deb_system, for the system install, and changes the 302# default scheme to a custom one pointing to /usr/local and replacing 303# site-packages with dist-packages. 304# See https://github.com/mesonbuild/meson/issues/8739. 305# XXX: We should be using sysconfig, but Debian only patches distutils. 306 307if 'deb_system' in distutils.command.install.INSTALL_SCHEMES: 308 paths = get_distutils_paths(scheme='deb_system') 309 install_paths = get_distutils_paths(scheme='deb_system', prefix='') 310else: 311 paths = sysconfig.get_paths() 312 empty_vars = {'base': '', 'platbase': '', 'installed_base': ''} 313 install_paths = sysconfig.get_paths(vars=empty_vars) 314 315def links_against_libpython(): 316 from distutils.core import Distribution, Extension 317 cmd = Distribution().get_command_obj('build_ext') 318 cmd.ensure_finalized() 319 return bool(cmd.get_libraries(Extension('dummy', []))) 320 321print(json.dumps({ 322 'variables': sysconfig.get_config_vars(), 323 'paths': paths, 324 'install_paths': install_paths, 325 'sys_paths': sys.path, 326 'version': sysconfig.get_python_version(), 327 'platform': sysconfig.get_platform(), 328 'is_pypy': '__pypy__' in sys.builtin_module_names, 329 'link_libpython': links_against_libpython(), 330})) 331''' 332 333if T.TYPE_CHECKING: 334 class PythonIntrospectionDict(TypedDict): 335 336 install_paths: T.Dict[str, str] 337 is_pypy: bool 338 link_libpython: bool 339 paths: T.Dict[str, str] 340 platform: str 341 suffix: str 342 variables: T.Dict[str, str] 343 version: str 344 345 346class PythonExternalProgram(ExternalProgram): 347 def __init__(self, name: str, command: T.Optional[T.List[str]] = None, 348 ext_prog: T.Optional[ExternalProgram] = None): 349 if ext_prog is None: 350 super().__init__(name, command=command, silent=True) 351 else: 352 self.name = name 353 self.command = ext_prog.command 354 self.path = ext_prog.path 355 356 # We want strong key values, so we always populate this with bogus data. 357 # Otherwise to make the type checkers happy we'd have to do .get() for 358 # everycall, even though we know that the introspection data will be 359 # complete 360 self.info: 'PythonIntrospectionDict' = { 361 'install_paths': {}, 362 'is_pypy': False, 363 'link_libpython': False, 364 'paths': {}, 365 'platform': 'sentinal', 366 'variables': {}, 367 'version': '0.0', 368 } 369 370 def _check_version(self, version: str) -> bool: 371 if self.name == 'python2': 372 return mesonlib.version_compare(version, '< 3.0') 373 elif self.name == 'python3': 374 return mesonlib.version_compare(version, '>= 3.0') 375 return True 376 377 def sanity(self, state: T.Optional['ModuleState'] = None) -> bool: 378 # Sanity check, we expect to have something that at least quacks in tune 379 cmd = self.get_command() + ['-c', INTROSPECT_COMMAND] 380 p, stdout, stderr = mesonlib.Popen_safe(cmd) 381 try: 382 info = json.loads(stdout) 383 except json.JSONDecodeError: 384 info = None 385 mlog.debug('Could not introspect Python (%s): exit code %d' % (str(p.args), p.returncode)) 386 mlog.debug('Program stdout:\n') 387 mlog.debug(stdout) 388 mlog.debug('Program stderr:\n') 389 mlog.debug(stderr) 390 391 if info is not None and self._check_version(info['version']): 392 variables = info['variables'] 393 info['suffix'] = variables.get('EXT_SUFFIX') or variables.get('SO') or variables.get('.so') 394 self.info = T.cast('PythonIntrospectionDict', info) 395 self.platlib = self._get_path(state, 'platlib') 396 self.purelib = self._get_path(state, 'purelib') 397 return True 398 else: 399 return False 400 401 def _get_path(self, state: T.Optional['ModuleState'], key: str) -> None: 402 if state: 403 value = state.get_option(f'{key}dir', module='python') 404 if value: 405 return value 406 user_dir = str(Path.home()) 407 sys_paths = self.info['sys_paths'] 408 rel_path = self.info['install_paths'][key][1:] 409 if not any(p.endswith(rel_path) for p in sys_paths if not p.startswith(user_dir)): 410 mlog.warning('Broken python installation detected. Python files', 411 'installed by Meson might not be found by python interpreter.\n', 412 f'This warning can be avoided by setting "python.{key}dir" option.', 413 once=True) 414 return rel_path 415 416 417_PURE_KW = KwargInfo('pure', bool, default=True) 418_SUBDIR_KW = KwargInfo('subdir', str, default='') 419 420if T.TYPE_CHECKING: 421 422 class PyInstallKw(TypedDict): 423 424 pure: bool 425 subdir: str 426 install_tag: T.Optional[str] 427 428 429class PythonInstallation(ExternalProgramHolder): 430 def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'): 431 ExternalProgramHolder.__init__(self, python, interpreter) 432 info = python.info 433 prefix = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('prefix')) 434 assert isinstance(prefix, str), 'for mypy' 435 self.variables = info['variables'] 436 self.suffix = info['suffix'] 437 self.paths = info['paths'] 438 self.platlib_install_path = os.path.join(prefix, python.platlib) 439 self.purelib_install_path = os.path.join(prefix, python.purelib) 440 self.version = info['version'] 441 self.platform = info['platform'] 442 self.is_pypy = info['is_pypy'] 443 self.link_libpython = info['link_libpython'] 444 self.methods.update({ 445 'extension_module': self.extension_module_method, 446 'dependency': self.dependency_method, 447 'install_sources': self.install_sources_method, 448 'get_install_dir': self.get_install_dir_method, 449 'language_version': self.language_version_method, 450 'found': self.found_method, 451 'has_path': self.has_path_method, 452 'get_path': self.get_path_method, 453 'has_variable': self.has_variable_method, 454 'get_variable': self.get_variable_method, 455 'path': self.path_method, 456 }) 457 458 @permittedKwargs(mod_kwargs) 459 def extension_module_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'SharedModule': 460 if 'install_dir' in kwargs: 461 if 'subdir' in kwargs: 462 raise InvalidArguments('"subdir" and "install_dir" are mutually exclusive') 463 else: 464 subdir = kwargs.pop('subdir', '') 465 if not isinstance(subdir, str): 466 raise InvalidArguments('"subdir" argument must be a string.') 467 468 kwargs['install_dir'] = os.path.join(self.platlib_install_path, subdir) 469 470 # On macOS and some Linux distros (Debian) distutils doesn't link 471 # extensions against libpython. We call into distutils and mirror its 472 # behavior. See https://github.com/mesonbuild/meson/issues/4117 473 if not self.link_libpython: 474 new_deps = [] 475 for dep in mesonlib.extract_as_list(kwargs, 'dependencies'): 476 if isinstance(dep, _PythonDependencyBase): 477 dep = dep.get_partial_dependency(compile_args=True) 478 new_deps.append(dep) 479 kwargs['dependencies'] = new_deps 480 481 # msys2's python3 has "-cpython-36m.dll", we have to be clever 482 # FIXME: explain what the specific cleverness is here 483 split, suffix = self.suffix.rsplit('.', 1) 484 args[0] += split 485 486 kwargs['name_prefix'] = '' 487 kwargs['name_suffix'] = suffix 488 489 return self.interpreter.func_shared_module(None, args, kwargs) 490 491 @disablerIfNotFound 492 @permittedKwargs(permitted_dependency_kwargs | {'embed'}) 493 @FeatureNewKwargs('python_installation.dependency', '0.53.0', ['embed']) 494 @noPosargs 495 def dependency_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'Dependency': 496 disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) 497 498 # it's theoretically (though not practically) possible for the else clse 499 # to not bind dep, let's ensure it is. 500 dep: 'Dependency' = NotFoundDependency(self.interpreter.environment) 501 if disabled: 502 mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled') 503 else: 504 new_kwargs = kwargs.copy() 505 new_kwargs['required'] = False 506 methods = process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs) 507 for d in python_factory(self.interpreter.environment, 508 MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST, 509 new_kwargs, methods, self): 510 dep = d() 511 if dep.found(): 512 break 513 if required and not dep.found(): 514 raise mesonlib.MesonException('Python dependency not found') 515 516 return dep 517 518 @typed_pos_args('install_data', varargs=(str, mesonlib.File)) 519 @typed_kwargs('python_installation.install_sources', _PURE_KW, _SUBDIR_KW, 520 KwargInfo('install_tag', (str, NoneType), since='0.60.0')) 521 def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]], 522 kwargs: 'PyInstallKw') -> 'Data': 523 tag = kwargs['install_tag'] or 'runtime' 524 return self.interpreter.install_data_impl( 525 self.interpreter.source_strings_to_files(args[0]), 526 self._get_install_dir_impl(kwargs['pure'], kwargs['subdir']), 527 mesonlib.FileMode(), rename=None, tag=tag, install_data_type='python', 528 install_dir_name=self._get_install_dir_name_impl(kwargs['pure'], kwargs['subdir'])) 529 530 @noPosargs 531 @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) 532 def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str: 533 return self._get_install_dir_impl(kwargs['pure'], kwargs['subdir']) 534 535 def _get_install_dir_impl(self, pure: bool, subdir: str) -> str: 536 return os.path.join( 537 self.purelib_install_path if pure else self.platlib_install_path, subdir) 538 539 def _get_install_dir_name_impl(self, pure: bool, subdir: str) -> str: 540 return os.path.join('{py_purelib}' if pure else '{py_platlib}', subdir) 541 542 @noPosargs 543 @noKwargs 544 def language_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: 545 return self.version 546 547 @typed_pos_args('python_installation.has_path', str) 548 @noKwargs 549 def has_path_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: 550 return args[0] in self.paths 551 552 @typed_pos_args('python_installation.get_path', str, optargs=[object]) 553 @noKwargs 554 def get_path_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': 555 path_name, fallback = args 556 try: 557 return self.paths[path_name] 558 except KeyError: 559 if fallback is not None: 560 return fallback 561 raise InvalidArguments(f'{path_name} is not a valid path name') 562 563 @typed_pos_args('python_installation.has_variable', str) 564 @noKwargs 565 def has_variable_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: 566 return args[0] in self.variables 567 568 @typed_pos_args('python_installation.get_variable', str, optargs=[object]) 569 @noKwargs 570 def get_variable_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': 571 var_name, fallback = args 572 try: 573 return self.variables[var_name] 574 except KeyError: 575 if fallback is not None: 576 return fallback 577 raise InvalidArguments(f'{var_name} is not a valid variable name') 578 579 @noPosargs 580 @noKwargs 581 @FeatureNew('Python module path method', '0.50.0') 582 def path_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: 583 return super().path_method(args, kwargs) 584 585 586if T.TYPE_CHECKING: 587 from ..interpreter.kwargs import ExtractRequired 588 589 class FindInstallationKw(ExtractRequired): 590 591 disabler: bool 592 modules: T.List[str] 593 594 595class PythonModule(ExtensionModule): 596 597 @FeatureNew('Python Module', '0.46.0') 598 def __init__(self, interpreter: 'Interpreter') -> None: 599 super().__init__(interpreter) 600 self.methods.update({ 601 'find_installation': self.find_installation, 602 }) 603 604 # https://www.python.org/dev/peps/pep-0397/ 605 @staticmethod 606 def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]: 607 if name_or_path not in ['python2', 'python3']: 608 return None 609 if not shutil.which('py'): 610 # program not installed, return without an exception 611 return None 612 ver = {'python2': '-2', 'python3': '-3'}[name_or_path] 613 cmd = ['py', ver, '-c', "import sysconfig; print(sysconfig.get_config_var('BINDIR'))"] 614 _, stdout, _ = mesonlib.Popen_safe(cmd) 615 directory = stdout.strip() 616 if os.path.exists(directory): 617 return os.path.join(directory, 'python') 618 else: 619 return None 620 621 @disablerIfNotFound 622 @typed_pos_args('python.find_installation', optargs=[str]) 623 @typed_kwargs( 624 'python.find_installation', 625 KwargInfo('required', (bool, UserFeatureOption), default=True), 626 KwargInfo('disabler', bool, default=False, since='0.49.0'), 627 KwargInfo('modules', ContainerTypeInfo(list, str), listify=True, default=[], since='0.51.0'), 628 ) 629 def find_installation(self, state: 'ModuleState', args: T.Tuple[T.Optional[str]], 630 kwargs: 'FindInstallationKw') -> ExternalProgram: 631 feature_check = FeatureNew('Passing "feature" option to find_installation', '0.48.0') 632 disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, feature_check) 633 want_modules = kwargs['modules'] 634 found_modules: T.List[str] = [] 635 missing_modules: T.List[str] = [] 636 637 # FIXME: this code is *full* of sharp corners. It assumes that it's 638 # going to get a string value (or now a list of length 1), of `python2` 639 # or `python3` which is completely nonsense. On windows the value could 640 # easily be `['py', '-3']`, or `['py', '-3.7']` to get a very specific 641 # version of python. On Linux we might want a python that's not in 642 # $PATH, or that uses a wrapper of some kind. 643 np: T.List[str] = state.environment.lookup_binary_entry(MachineChoice.HOST, 'python') or [] 644 fallback = args[0] 645 display_name = fallback or 'python' 646 if not np and fallback is not None: 647 np = [fallback] 648 name_or_path = np[0] if np else None 649 650 if disabled: 651 mlog.log('Program', name_or_path or 'python', 'found:', mlog.red('NO'), '(disabled by:', mlog.bold(feature), ')') 652 return NonExistingExternalProgram() 653 654 if not name_or_path: 655 python = PythonExternalProgram('python3', mesonlib.python_command) 656 else: 657 tmp_python = ExternalProgram.from_entry(display_name, name_or_path) 658 python = PythonExternalProgram(display_name, ext_prog=tmp_python) 659 660 if not python.found() and mesonlib.is_windows(): 661 pythonpath = self._get_win_pythonpath(name_or_path) 662 if pythonpath is not None: 663 name_or_path = pythonpath 664 python = PythonExternalProgram(name_or_path) 665 666 # Last ditch effort, python2 or python3 can be named python 667 # on various platforms, let's not give up just yet, if an executable 668 # named python is available and has a compatible version, let's use 669 # it 670 if not python.found() and name_or_path in ['python2', 'python3']: 671 python = PythonExternalProgram('python') 672 673 if python.found() and want_modules: 674 for mod in want_modules: 675 p, *_ = mesonlib.Popen_safe( 676 python.command + 677 ['-c', f'import {mod}']) 678 if p.returncode != 0: 679 missing_modules.append(mod) 680 else: 681 found_modules.append(mod) 682 683 msg: T.List['mlog.TV_Loggable'] = ['Program', python.name] 684 if want_modules: 685 msg.append('({})'.format(', '.join(want_modules))) 686 msg.append('found:') 687 if python.found() and not missing_modules: 688 msg.extend([mlog.green('YES'), '({})'.format(' '.join(python.command))]) 689 else: 690 msg.append(mlog.red('NO')) 691 if found_modules: 692 msg.append('modules:') 693 msg.append(', '.join(found_modules)) 694 695 mlog.log(*msg) 696 697 if not python.found(): 698 if required: 699 raise mesonlib.MesonException('{} not found'.format(name_or_path or 'python')) 700 return NonExistingExternalProgram() 701 elif missing_modules: 702 if required: 703 raise mesonlib.MesonException('{} is missing modules: {}'.format(name_or_path or 'python', ', '.join(missing_modules))) 704 return NonExistingExternalProgram() 705 else: 706 sane = python.sanity(state) 707 708 if sane: 709 return python 710 else: 711 if required: 712 raise mesonlib.MesonException(f'{python} is not a valid python or it is missing distutils') 713 return NonExistingExternalProgram() 714 715 raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).') 716 717 718def initialize(interpreter: 'Interpreter') -> PythonModule: 719 mod = PythonModule(interpreter) 720 mod.interpreter.append_holder_map(PythonExternalProgram, PythonInstallation) 721 return mod 722