| # Copyright 2018 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. |
| |
| import os |
| import json |
| |
| from pathlib import Path |
| from .. import mesonlib |
| from . import ExtensionModule |
| from mesonbuild.modules import ModuleReturnValue |
| from ..interpreterbase import ( |
| noPosargs, noKwargs, permittedKwargs, |
| InterpreterObject, InvalidArguments, |
| FeatureNew |
| ) |
| from ..interpreter import ExternalProgramHolder |
| from ..interpreterbase import flatten |
| from ..build import known_shmod_kwargs |
| from .. import mlog |
| from ..environment import detect_cpu_family |
| from ..dependencies.base import ( |
| DependencyMethods, ExternalDependency, |
| ExternalProgram, PkgConfigDependency, |
| NonExistingExternalProgram |
| ) |
| |
| mod_kwargs = set(['subdir']) |
| mod_kwargs.update(known_shmod_kwargs) |
| mod_kwargs -= set(['name_prefix', 'name_suffix']) |
| |
| |
| def run_command(python, command): |
| _, stdout, _ = mesonlib.Popen_safe(python.get_command() + [ |
| '-c', |
| command]) |
| |
| return stdout.strip() |
| |
| |
| class PythonDependency(ExternalDependency): |
| |
| def __init__(self, python_holder, environment, kwargs): |
| super().__init__('python', environment, None, kwargs) |
| self.name = 'python' |
| self.static = kwargs.get('static', False) |
| self.version = python_holder.version |
| self.platform = python_holder.platform |
| self.pkgdep = None |
| self.variables = python_holder.variables |
| self.paths = python_holder.paths |
| if mesonlib.version_compare(self.version, '>= 3.0'): |
| self.major_version = 3 |
| else: |
| self.major_version = 2 |
| |
| 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') |
| old_pkg_libdir = os.environ.get('PKG_CONFIG_LIBDIR') |
| old_pkg_path = os.environ.get('PKG_CONFIG_PATH') |
| |
| os.environ.pop('PKG_CONFIG_PATH', None) |
| |
| if pkg_libdir: |
| os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir |
| |
| try: |
| self.pkgdep = PkgConfigDependency('python-{}'.format(pkg_version), environment, kwargs) |
| except Exception: |
| pass |
| |
| if old_pkg_path is not None: |
| os.environ['PKG_CONFIG_PATH'] = old_pkg_path |
| |
| if old_pkg_libdir is not None: |
| os.environ['PKG_CONFIG_LIBDIR'] = old_pkg_libdir |
| else: |
| os.environ.pop('PKG_CONFIG_LIBDIR', None) |
| |
| 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 |
| |
| 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.log('Dependency', mlog.bold(self.name), 'found:', mlog.green('YES')) |
| else: |
| mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.red('NO')) |
| |
| def _find_libpy(self, python_holder, environment): |
| if python_holder.is_pypy: |
| if self.major_version == 3: |
| libname = 'pypy3-c' |
| else: |
| libname = 'pypy-c' |
| libdir = os.path.join(self.variables.get('base'), 'bin') |
| libdirs = [libdir] |
| else: |
| libname = 'python{}'.format(self.version) |
| if 'DEBUG_EXT' in self.variables: |
| libname += self.variables['DEBUG_EXT'] |
| if 'ABIFLAGS' in self.variables: |
| libname += self.variables['ABIFLAGS'] |
| libdirs = [] |
| |
| largs = self.clib_compiler.find_library(libname, environment, libdirs, []) |
| |
| self.is_found = largs is not None |
| |
| self.link_args = largs |
| |
| inc_paths = mesonlib.OrderedSet([ |
| self.variables.get('INCLUDEPY'), |
| self.paths.get('include'), |
| self.paths.get('platinclude')]) |
| |
| self.compile_args += ['-I' + path for path in inc_paths if path] |
| |
| def get_windows_python_arch(self): |
| if self.platform == 'mingw': |
| pycc = self.variables.get('CC') |
| if pycc.startswith('x86_64'): |
| return '64' |
| elif pycc.startswith(('i686', 'i386')): |
| return '32' |
| else: |
| mlog.log('MinGW Python built with unknown CC {!r}, please file' |
| 'a bug'.format(pycc)) |
| return None |
| elif self.platform == 'win32': |
| return '32' |
| elif self.platform in ('win64', 'win-amd64'): |
| return '64' |
| mlog.log('Unknown Windows Python platform {!r}'.format(self.platform)) |
| return None |
| |
| def get_windows_link_args(self): |
| if self.platform.startswith('win'): |
| vernum = self.variables.get('py_version_nodot') |
| if self.static: |
| libname = 'libpython{}.a'.format(vernum) |
| else: |
| libname = 'python{}.lib'.format(vernum) |
| lib = Path(self.variables.get('base')) / 'libs' / libname |
| elif self.platform == 'mingw': |
| if self.static: |
| libname = self.variables.get('LIBRARY') |
| else: |
| libname = self.variables.get('LDLIBRARY') |
| lib = Path(self.variables.get('LIBDIR')) / libname |
| 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): |
| ''' |
| Find python3 libraries on Windows and also verify that the arch matches |
| what we are building for. |
| ''' |
| pyarch = self.get_windows_python_arch() |
| if pyarch is None: |
| self.is_found = False |
| return |
| arch = detect_cpu_family(env.coredata.compilers) |
| if arch == 'x86': |
| arch = '32' |
| elif arch == 'x86_64': |
| arch = '64' |
| else: |
| # We can't cross-compile Python 3 dependencies on Windows yet |
| mlog.log('Unknown architecture {!r} for'.format(arch), |
| mlog.bold(self.name)) |
| self.is_found = False |
| return |
| # Pyarch ends in '32' or '64' |
| if arch != pyarch: |
| mlog.log('Need', mlog.bold(self.name), 'for {}-bit, but ' |
| 'found {}-bit'.format(arch, pyarch)) |
| self.is_found = False |
| return |
| # This can fail if the library is not found |
| largs = self.get_windows_link_args() |
| if largs is None: |
| self.is_found = False |
| return |
| self.link_args = largs |
| # Compile args |
| inc_paths = mesonlib.OrderedSet([ |
| self.variables.get('INCLUDEPY'), |
| self.paths.get('include'), |
| self.paths.get('platinclude')]) |
| |
| self.compile_args += ['-I' + path for path in inc_paths if path] |
| |
| # https://sourceforge.net/p/mingw-w64/mailman/message/30504611/ |
| if pyarch == '64' and self.major_version == 2: |
| self.compile_args += ['-DMS_WIN64'] |
| |
| 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) |
| |
| |
| VARIABLES_COMMAND = ''' |
| import sysconfig |
| import json |
| |
| print (json.dumps (sysconfig.get_config_vars())) |
| ''' |
| |
| |
| PATHS_COMMAND = ''' |
| import sysconfig |
| import json |
| |
| print (json.dumps(sysconfig.get_paths())) |
| ''' |
| |
| |
| INSTALL_PATHS_COMMAND = ''' |
| import sysconfig |
| import json |
| |
| print (json.dumps(sysconfig.get_paths(scheme='posix_prefix', vars={'base': '', 'platbase': '', 'installed_base': ''}))) |
| ''' |
| |
| |
| IS_PYPY_COMMAND = ''' |
| import sys |
| import json |
| |
| print (json.dumps('__pypy__' in sys.builtin_module_names)) |
| ''' |
| |
| |
| class PythonInstallation(ExternalProgramHolder, InterpreterObject): |
| def __init__(self, interpreter, python): |
| InterpreterObject.__init__(self) |
| ExternalProgramHolder.__init__(self, python) |
| self.interpreter = interpreter |
| prefix = self.interpreter.environment.coredata.get_builtin_option('prefix') |
| self.variables = json.loads(run_command(python, VARIABLES_COMMAND)) |
| self.paths = json.loads(run_command(python, PATHS_COMMAND)) |
| install_paths = json.loads(run_command(python, INSTALL_PATHS_COMMAND)) |
| self.platlib_install_path = os.path.join(prefix, install_paths['platlib'][1:]) |
| self.purelib_install_path = os.path.join(prefix, install_paths['purelib'][1:]) |
| self.version = run_command(python, "import sysconfig; print (sysconfig.get_python_version())") |
| self.platform = run_command(python, "import sysconfig; print (sysconfig.get_platform())") |
| self.is_pypy = json.loads(run_command(python, IS_PYPY_COMMAND)) |
| |
| @permittedKwargs(mod_kwargs) |
| def extension_module(self, interpreter, state, args, kwargs): |
| if 'subdir' in kwargs and 'install_dir' in kwargs: |
| raise InvalidArguments('"subdir" and "install_dir" are mutually exclusive') |
| |
| if 'subdir' in kwargs: |
| subdir = kwargs.pop('subdir', '') |
| if not isinstance(subdir, str): |
| raise InvalidArguments('"subdir" argument must be a string.') |
| |
| kwargs['install_dir'] = os.path.join(self.platlib_install_path, subdir) |
| |
| suffix = self.variables.get('EXT_SUFFIX') or self.variables.get('SO') or self.variables.get('.so') |
| |
| # msys2's python3 has "-cpython-36m.dll", we have to be clever |
| split = suffix.rsplit('.', 1) |
| suffix = split.pop(-1) |
| args[0] += ''.join(s for s in split) |
| |
| kwargs['name_prefix'] = '' |
| kwargs['name_suffix'] = suffix |
| |
| return interpreter.func_shared_module(None, args, kwargs) |
| |
| def dependency(self, interpreter, state, args, kwargs): |
| dep = PythonDependency(self, interpreter.environment, kwargs) |
| return interpreter.holderify(dep) |
| |
| @permittedKwargs(['pure', 'subdir']) |
| def install_sources(self, interpreter, state, args, kwargs): |
| pure = kwargs.pop('pure', False) |
| 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 interpreter.func_install_data(None, args, kwargs) |
| |
| @noPosargs |
| @permittedKwargs(['pure', 'subdir']) |
| def get_install_dir(self, node, 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) |
| |
| return ModuleReturnValue(res, []) |
| |
| @noPosargs |
| @noKwargs |
| def language_version(self, node, args, kwargs): |
| return ModuleReturnValue(self.version, []) |
| |
| @noPosargs |
| @noKwargs |
| def found(self, node, args, kwargs): |
| return ModuleReturnValue(True, []) |
| |
| @noKwargs |
| def has_path(self, node, 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 ModuleReturnValue(path_name in self.paths, []) |
| |
| @noKwargs |
| def get_path(self, node, 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.') |
| |
| try: |
| path = self.paths[path_name] |
| except KeyError: |
| if len(args) == 2: |
| path = args[1] |
| else: |
| raise InvalidArguments('{} is not a valid path name'.format(path_name)) |
| |
| return ModuleReturnValue(path, []) |
| |
| @noKwargs |
| def has_variable(self, node, 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 ModuleReturnValue(var_name in self.variables, []) |
| |
| @noKwargs |
| def get_variable(self, node, 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.') |
| |
| try: |
| var = self.variables[var_name] |
| except KeyError: |
| if len(args) == 2: |
| var = args[1] |
| else: |
| raise InvalidArguments('{} is not a valid variable name'.format(var_name)) |
| |
| return ModuleReturnValue(var, []) |
| |
| def method_call(self, method_name, args, kwargs): |
| try: |
| fn = getattr(self, method_name) |
| except AttributeError: |
| raise InvalidArguments('Python object does not have method %s.' % method_name) |
| |
| if not getattr(fn, 'no-args-flattening', False): |
| args = flatten(args) |
| |
| if method_name in ['extension_module', 'dependency', 'install_sources']: |
| value = fn(self.interpreter, None, args, kwargs) |
| return self.interpreter.holderify(value) |
| elif method_name in ['has_variable', 'get_variable', 'has_path', 'get_path', 'found', 'language_version', 'get_install_dir']: |
| value = fn(None, args, kwargs) |
| return self.interpreter.module_method_callback(value) |
| else: |
| raise InvalidArguments('Python object does not have method %s.' % method_name) |
| |
| |
| class PythonModule(ExtensionModule): |
| |
| @FeatureNew('Python Module', '0.46.0') |
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| self.snippets.add('find_installation') |
| |
| # https://www.python.org/dev/peps/pep-0397/ |
| def _get_win_pythonpath(self, name_or_path): |
| if name_or_path not in ['python2', 'python3']: |
| return None |
| ver = {'python2': '-2', 'python3': '-3'}[name_or_path] |
| cmd = ['py', ver, '-c', "import sysconfig; print(sysconfig.get_config_var('BINDIR'))"] |
| _, stdout, _ = mesonlib.Popen_safe(cmd) |
| dir = stdout.strip() |
| if os.path.exists(dir): |
| return os.path.join(dir, 'python') |
| else: |
| return None |
| |
| @permittedKwargs(['required']) |
| def find_installation(self, interpreter, state, args, kwargs): |
| required = kwargs.get('required', True) |
| if not isinstance(required, bool): |
| raise InvalidArguments('"required" argument must be a boolean.') |
| |
| if len(args) > 1: |
| raise InvalidArguments('find_installation takes zero or one positional argument.') |
| |
| if args: |
| name_or_path = args[0] |
| if not isinstance(name_or_path, str): |
| raise InvalidArguments('find_installation argument must be a string.') |
| else: |
| name_or_path = None |
| |
| if not name_or_path: |
| mlog.log("Using meson's python {}".format(mesonlib.python_command)) |
| python = ExternalProgram('python3', mesonlib.python_command, silent=True) |
| else: |
| if mesonlib.is_windows(): |
| pythonpath = self._get_win_pythonpath(name_or_path) |
| if pythonpath is not None: |
| name_or_path = pythonpath |
| python = ExternalProgram(name_or_path, silent = True) |
| # Last ditch effort, python2 or python3 can be named python |
| # on various platforms, let's not give up just yet, if an executable |
| # named python is available and has a compatible version, let's use |
| # it |
| if not python.found() and name_or_path in ['python2', 'python3']: |
| python = ExternalProgram('python', silent = True) |
| if python.found(): |
| version = run_command(python, "import sysconfig; print (sysconfig.get_python_version())") |
| if not version or \ |
| name_or_path == 'python2' and mesonlib.version_compare(version, '>= 3.0') or \ |
| name_or_path == 'python3' and not mesonlib.version_compare(version, '>= 3.0'): |
| python = NonExistingExternalProgram() |
| |
| if not python.found(): |
| if required: |
| raise mesonlib.MesonException('{} not found'.format(name_or_path or 'python')) |
| res = ExternalProgramHolder(NonExistingExternalProgram()) |
| else: |
| # Sanity check, we expect to have something that at least quacks in tune |
| version = run_command(python, "import sysconfig; print (sysconfig.get_python_version())") |
| if not version: |
| res = ExternalProgramHolder(NonExistingExternalProgram()) |
| if required: |
| raise mesonlib.MesonException('{} is not a valid python'.format(python)) |
| else: |
| res = PythonInstallation(interpreter, python) |
| |
| return res |
| |
| |
| def initialize(*args, **kwargs): |
| return PythonModule(*args, **kwargs) |