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