The Meson Build System http://mesonbuild.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

612 lines
25 KiB

# 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
import shutil
import typing as T
from pathlib import Path
from .. import mesonlib
from ..mesonlib import MachineChoice, MesonException
from . import ExtensionModule
from ..interpreterbase import (
noPosargs, noKwargs, permittedKwargs,
InvalidArguments,
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 ..programs import ExternalProgram, NonExistingExternalProgram
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
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
self.info: T.Optional[T.Dict[str, str]] = None
if mesonlib.version_compare(self.version, '>= 3.0'):
self.major_version = 3
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')
os.environ.pop('PKG_CONFIG_PATH', None)
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)
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)
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'
if self.is_found:
mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.green(f'YES ({py_lookup_method})'))
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 = f'python{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)
if largs is not None:
self.link_args = largs
self.is_found = largs is not None or self.link_libpython
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(f'Unknown Windows Python platform {self.platform!r}')
return None
def get_windows_link_args(self):
if self.platform.startswith('win'):
vernum = self.variables.get('py_version_nodot')
if self.static:
libpath = Path('libs') / f'libpython{vernum}.a'
else:
comp = self.get_compiler()
if comp.id == "gcc":
libpath = f'python{vernum}.dll'
else:
libpath = Path('libs') / f'python{vernum}.lib'
lib = Path(self.variables.get('base')) / libpath
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.host)
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(f'Unknown architecture {arch!r} for',
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)
INTROSPECT_COMMAND = '''import sysconfig
import json
import sys
install_paths = sysconfig.get_paths(scheme='posix_prefix', vars={'base': '', 'platbase': '', 'installed_base': ''})
def links_against_libpython():
from distutils.core import Distribution, Extension
cmd = Distribution().get_command_obj('build_ext')
cmd.ensure_finalized()
return bool(cmd.get_libraries(Extension('dummy', [])))
print (json.dumps ({
'variables': sysconfig.get_config_vars(),
'paths': sysconfig.get_paths(),
'install_paths': install_paths,
'version': sysconfig.get_python_version(),
'platform': sysconfig.get_platform(),
'is_pypy': '__pypy__' in sys.builtin_module_names,
'link_libpython': links_against_libpython(),
}))
'''
class PythonExternalProgram(ExternalProgram):
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] = {}
class PythonInstallation(ExternalProgramHolder):
def __init__(self, python, interpreter):
ExternalProgramHolder.__init__(self, python, interpreter)
info = python.info
prefix = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('prefix'))
self.variables = info['variables']
self.paths = info['paths']
install_paths = info['install_paths']
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 = info['version']
self.platform = info['platform']
self.is_pypy = info['is_pypy']
self.link_libpython = info['link_libpython']
self.methods.update({
'extension_module': self.extension_module_method,
'dependency': self.dependency_method,
'install_sources': self.install_sources_method,
'get_install_dir': self.get_install_dir_method,
'language_version': self.language_version_method,
'found': self.found_method,
'has_path': self.has_path_method,
'get_path': self.get_path_method,
'has_variable': self.has_variable_method,
'get_variable': self.get_variable_method,
'path': self.path_method,
})
@permittedKwargs(mod_kwargs)
def extension_module_method(self, 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)
# On macOS and some Linux distros (Debian) distutils doesn't link
# extensions against libpython. We call into distutils and mirror its
# behavior. See https://github.com/mesonbuild/meson/issues/4117
if not self.link_libpython:
new_deps = []
for dep in mesonlib.extract_as_list(kwargs, 'dependencies'):
if isinstance(dep, PythonDependency):
dep = dep.get_partial_dependency(compile_args=True)
new_deps.append(dep)
kwargs['dependencies'] = new_deps
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 self.interpreter.func_shared_module(None, args, kwargs)
@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)
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
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)
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 self.interpreter.func_install_data(None, args, kwargs)
@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)
return res
@noPosargs
@noKwargs
def language_version_method(self, args, kwargs):
return self.version
@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
@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.')
try:
path = 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
@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
@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.')
try:
var = 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
@noPosargs
@noKwargs
@FeatureNew('Python module path method', '0.50.0')
def path_method(self, args, kwargs):
return super().path_method(args, kwargs)
class PythonModule(ExtensionModule):
@FeatureNew('Python Module', '0.46.0')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.methods.update({
'find_installation': self.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
if not shutil.which('py'):
# program not installed, return without an exception
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)
directory = stdout.strip()
if os.path.exists(directory):
return os.path.join(directory, 'python')
else:
return None
def _check_version(self, name_or_path, version):
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):
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.')
if disabled:
mlog.log('Program', name_or_path or 'python', 'found:', mlog.red('NO'), '(disabled by:', mlog.bold(feature), ')')
return NonExistingExternalProgram()
if not name_or_path:
python = PythonExternalProgram('python3', mesonlib.python_command)
else:
tmp_python = ExternalProgram.from_entry('python3', name_or_path)
python = PythonExternalProgram('python3', ext_prog=tmp_python)
if not python.found() and mesonlib.is_windows():
pythonpath = self._get_win_pythonpath(name_or_path)
if pythonpath is not None:
name_or_path = pythonpath
python = PythonExternalProgram(name_or_path)
# 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 = PythonExternalProgram('python')
if python.found() and want_modules:
for mod in want_modules:
p, out, err = mesonlib.Popen_safe(
python.command +
['-c', f'import {mod}'])
if p.returncode != 0:
missing_modules.append(mod)
else:
found_modules.append(mod)
msg = ['Program', python.name]
if want_modules:
msg.append('({})'.format(', '.join(want_modules)))
msg.append('found:')
if python.found() and not missing_modules:
msg.extend([mlog.green('YES'), '({})'.format(' '.join(python.command))])
else:
msg.append(mlog.red('NO'))
if found_modules:
msg.append('modules:')
msg.append(', '.join(found_modules))
mlog.log(*msg)
if not python.found():
if required:
raise mesonlib.MesonException('{} not found'.format(name_or_path or 'python'))
return NonExistingExternalProgram()
elif missing_modules:
if required:
raise mesonlib.MesonException('{} is missing modules: {}'.format(name_or_path or 'python', ', '.join(missing_modules)))
return NonExistingExternalProgram()
else:
# Sanity check, we expect to have something that at least quacks in tune
try:
cmd = python.get_command() + ['-c', INTROSPECT_COMMAND]
p, stdout, stderr = mesonlib.Popen_safe(cmd)
info = json.loads(stdout)
except json.JSONDecodeError:
info = None
mlog.debug('Could not introspect Python (%s): exit code %d' % (str(p.args), p.returncode))
mlog.debug('Program stdout:\n')
mlog.debug(stdout)
mlog.debug('Program stderr:\n')
mlog.debug(stderr)
if isinstance(info, dict) and 'version' in info and self._check_version(name_or_path, info['version']):
python.info = info
return python
else:
if required:
raise mesonlib.MesonException(f'{python} is not a valid python or it is missing setuptools')
return NonExistingExternalProgram()
raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).')
def initialize(*args, **kwargs):
mod = PythonModule(*args, **kwargs)
mod.interpreter.append_holder_map(PythonExternalProgram, PythonInstallation)
return mod