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.
491 lines
21 KiB
491 lines
21 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. |
|
from __future__ import annotations |
|
|
|
import copy, json, os, shutil |
|
import typing as T |
|
|
|
from . import ExtensionModule, ModuleInfo |
|
from .. import mesonlib |
|
from .. import mlog |
|
from ..coredata import UserFeatureOption |
|
from ..build import known_shmod_kwargs |
|
from ..dependencies import NotFoundDependency |
|
from ..dependencies.detect import get_dep_identifier |
|
from ..dependencies.python import BasicPythonExternalProgram, python_factory, _PythonDependencyBase |
|
from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs |
|
from ..interpreter import primitives as P_OBJ |
|
from ..interpreter.type_checking import NoneType, PRESERVE_PATH_KW |
|
from ..interpreterbase import ( |
|
noPosargs, noKwargs, permittedKwargs, ContainerTypeInfo, |
|
InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo, |
|
FeatureNew, FeatureNewKwargs, disablerIfNotFound |
|
) |
|
from ..mesonlib import MachineChoice |
|
from ..programs import ExternalProgram, NonExistingExternalProgram |
|
|
|
if T.TYPE_CHECKING: |
|
from typing_extensions import TypedDict |
|
|
|
from . import ModuleState |
|
from ..build import Build, SharedModule, Data |
|
from ..dependencies import Dependency |
|
from ..interpreter import Interpreter |
|
from ..interpreter.kwargs import ExtractRequired |
|
from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs |
|
|
|
class PyInstallKw(TypedDict): |
|
|
|
pure: T.Optional[bool] |
|
subdir: str |
|
install_tag: T.Optional[str] |
|
|
|
class FindInstallationKw(ExtractRequired): |
|
|
|
disabler: bool |
|
modules: T.List[str] |
|
pure: T.Optional[bool] |
|
|
|
|
|
mod_kwargs = {'subdir'} |
|
mod_kwargs.update(known_shmod_kwargs) |
|
mod_kwargs -= {'name_prefix', 'name_suffix'} |
|
|
|
|
|
class PythonExternalProgram(BasicPythonExternalProgram): |
|
|
|
# This is a ClassVar instead of an instance bool, because although an |
|
# installation is cached, we actually copy it, modify attributes such as pure, |
|
# and return a temporary one rather than the cached object. |
|
run_bytecompile: T.ClassVar[T.Dict[str, bool]] = {} |
|
|
|
def sanity(self, state: T.Optional['ModuleState'] = None) -> bool: |
|
ret = super().sanity() |
|
if ret: |
|
self.platlib = self._get_path(state, 'platlib') |
|
self.purelib = self._get_path(state, 'purelib') |
|
return ret |
|
|
|
def _get_path(self, state: T.Optional['ModuleState'], key: str) -> None: |
|
rel_path = self.info['install_paths'][key][1:] |
|
if not state: |
|
# This happens only from run_project_tests.py |
|
return rel_path |
|
value = state.get_option(f'{key}dir', module='python') |
|
if value: |
|
if state.is_user_defined_option('install_env', module='python'): |
|
raise mesonlib.MesonException(f'python.{key}dir and python.install_env are mutually exclusive') |
|
return value |
|
|
|
install_env = state.get_option('install_env', module='python') |
|
if install_env == 'auto': |
|
install_env = 'venv' if self.info['is_venv'] else 'system' |
|
|
|
if install_env == 'system': |
|
rel_path = os.path.join(self.info['variables']['prefix'], rel_path) |
|
elif install_env == 'venv': |
|
if not self.info['is_venv']: |
|
raise mesonlib.MesonException('python.install_env cannot be set to "venv" unless you are in a venv!') |
|
# inside a venv, deb_system is *never* active hence info['paths'] may be wrong |
|
rel_path = self.info['sysconfig_paths'][key] |
|
|
|
return rel_path |
|
|
|
|
|
_PURE_KW = KwargInfo('pure', (bool, NoneType)) |
|
_SUBDIR_KW = KwargInfo('subdir', str, default='') |
|
|
|
|
|
class PythonInstallation(ExternalProgramHolder): |
|
def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'): |
|
ExternalProgramHolder.__init__(self, python, interpreter) |
|
info = python.info |
|
prefix = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('prefix')) |
|
assert isinstance(prefix, str), 'for mypy' |
|
self.variables = info['variables'] |
|
self.suffix = info['suffix'] |
|
self.paths = info['paths'] |
|
self.pure = python.pure |
|
self.platlib_install_path = os.path.join(prefix, python.platlib) |
|
self.purelib_install_path = os.path.join(prefix, python.purelib) |
|
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: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'SharedModule': |
|
if 'install_dir' in kwargs: |
|
if 'subdir' in kwargs: |
|
raise InvalidArguments('"subdir" and "install_dir" are mutually exclusive') |
|
else: |
|
subdir = kwargs.pop('subdir', '') |
|
if not isinstance(subdir, str): |
|
raise InvalidArguments('"subdir" argument must be a string.') |
|
|
|
kwargs['install_dir'] = self._get_install_dir_impl(False, subdir) |
|
|
|
new_deps = mesonlib.extract_as_list(kwargs, 'dependencies') |
|
has_pydep = any(isinstance(dep, _PythonDependencyBase) for dep in new_deps) |
|
if not has_pydep: |
|
pydep = self._dependency_method_impl({}) |
|
if not pydep.found(): |
|
raise mesonlib.MesonException('Python dependency not found') |
|
new_deps.append(pydep) |
|
FeatureNew.single_use('python_installation.extension_module with implicit dependency on python', |
|
'0.63.0', self.subproject, 'use python_installation.dependency()', |
|
self.current_node) |
|
kwargs['dependencies'] = new_deps |
|
|
|
# msys2's python3 has "-cpython-36m.dll", we have to be clever |
|
# FIXME: explain what the specific cleverness is here |
|
split, suffix = self.suffix.rsplit('.', 1) |
|
args[0] += split |
|
|
|
kwargs['name_prefix'] = '' |
|
kwargs['name_suffix'] = suffix |
|
|
|
if 'gnu_symbol_visibility' not in kwargs and \ |
|
(self.is_pypy or mesonlib.version_compare(self.version, '>=3.9')): |
|
kwargs['gnu_symbol_visibility'] = 'inlineshidden' |
|
|
|
return self.interpreter.func_shared_module(None, args, kwargs) |
|
|
|
def _dependency_method_impl(self, kwargs: TYPE_kwargs) -> Dependency: |
|
for_machine = self.interpreter.machine_from_native_kwarg(kwargs) |
|
identifier = get_dep_identifier(self._full_path(), kwargs) |
|
|
|
dep = self.interpreter.coredata.deps[for_machine].get(identifier) |
|
if dep is not None: |
|
return dep |
|
|
|
new_kwargs = kwargs.copy() |
|
new_kwargs['required'] = False |
|
# it's theoretically (though not practically) possible to not bind dep, let's ensure it is. |
|
dep: Dependency = NotFoundDependency('python', self.interpreter.environment) |
|
for d in python_factory(self.interpreter.environment, for_machine, new_kwargs, self.held_object): |
|
dep = d() |
|
if dep.found(): |
|
break |
|
|
|
self.interpreter.coredata.deps[for_machine].put(identifier, dep) |
|
return dep |
|
|
|
@disablerIfNotFound |
|
@permittedKwargs(permitted_dependency_kwargs | {'embed'}) |
|
@FeatureNewKwargs('python_installation.dependency', '0.53.0', ['embed']) |
|
@noPosargs |
|
def dependency_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'Dependency': |
|
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) |
|
if disabled: |
|
mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled') |
|
return NotFoundDependency('python', self.interpreter.environment) |
|
else: |
|
dep = self._dependency_method_impl(kwargs) |
|
if required and not dep.found(): |
|
raise mesonlib.MesonException('Python dependency not found') |
|
return dep |
|
|
|
@typed_pos_args('install_data', varargs=(str, mesonlib.File)) |
|
@typed_kwargs( |
|
'python_installation.install_sources', |
|
_PURE_KW, |
|
_SUBDIR_KW, |
|
PRESERVE_PATH_KW, |
|
KwargInfo('install_tag', (str, NoneType), since='0.60.0') |
|
) |
|
def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]], |
|
kwargs: 'PyInstallKw') -> 'Data': |
|
self.held_object.run_bytecompile[self.version] = True |
|
tag = kwargs['install_tag'] or 'python-runtime' |
|
pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure |
|
install_dir = self._get_install_dir_impl(pure, kwargs['subdir']) |
|
return self.interpreter.install_data_impl( |
|
self.interpreter.source_strings_to_files(args[0]), |
|
install_dir, |
|
mesonlib.FileMode(), rename=None, tag=tag, install_data_type='python', |
|
install_dir_name=install_dir.optname, |
|
preserve_path=kwargs['preserve_path']) |
|
|
|
@noPosargs |
|
@typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) |
|
def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str: |
|
self.held_object.run_bytecompile[self.version] = True |
|
pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure |
|
return self._get_install_dir_impl(pure, kwargs['subdir']) |
|
|
|
def _get_install_dir_impl(self, pure: bool, subdir: str) -> P_OBJ.OptionString: |
|
if pure: |
|
base = self.purelib_install_path |
|
name = '{py_purelib}' |
|
else: |
|
base = self.platlib_install_path |
|
name = '{py_platlib}' |
|
|
|
return P_OBJ.OptionString(os.path.join(base, subdir), os.path.join(name, subdir)) |
|
|
|
@noPosargs |
|
@noKwargs |
|
def language_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: |
|
return self.version |
|
|
|
@typed_pos_args('python_installation.has_path', str) |
|
@noKwargs |
|
def has_path_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: |
|
return args[0] in self.paths |
|
|
|
@typed_pos_args('python_installation.get_path', str, optargs=[object]) |
|
@noKwargs |
|
def get_path_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': |
|
path_name, fallback = args |
|
try: |
|
return self.paths[path_name] |
|
except KeyError: |
|
if fallback is not None: |
|
return fallback |
|
raise InvalidArguments(f'{path_name} is not a valid path name') |
|
|
|
@typed_pos_args('python_installation.has_variable', str) |
|
@noKwargs |
|
def has_variable_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: |
|
return args[0] in self.variables |
|
|
|
@typed_pos_args('python_installation.get_variable', str, optargs=[object]) |
|
@noKwargs |
|
def get_variable_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': |
|
var_name, fallback = args |
|
try: |
|
return self.variables[var_name] |
|
except KeyError: |
|
if fallback is not None: |
|
return fallback |
|
raise InvalidArguments(f'{var_name} is not a valid variable name') |
|
|
|
@noPosargs |
|
@noKwargs |
|
@FeatureNew('Python module path method', '0.50.0') |
|
def path_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: |
|
return super().path_method(args, kwargs) |
|
|
|
|
|
class PythonModule(ExtensionModule): |
|
|
|
INFO = ModuleInfo('python', '0.46.0') |
|
|
|
def __init__(self, interpreter: 'Interpreter') -> None: |
|
super().__init__(interpreter) |
|
self.installations: T.Dict[str, ExternalProgram] = {} |
|
self.methods.update({ |
|
'find_installation': self.find_installation, |
|
}) |
|
|
|
def _get_install_scripts(self) -> T.List[mesonlib.ExecutableSerialisation]: |
|
backend = self.interpreter.backend |
|
ret = [] |
|
optlevel = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('bytecompile', module='python')) |
|
if optlevel == -1: |
|
return ret |
|
if not any(PythonExternalProgram.run_bytecompile.values()): |
|
return ret |
|
|
|
installdata = backend.create_install_data() |
|
py_files = [] |
|
|
|
def should_append(f, isdir: bool = False): |
|
# This uses the install_plan decorated names to see if the original source was propagated via |
|
# install_sources() or get_install_dir(). |
|
return f.startswith(('{py_platlib}', '{py_purelib}')) and (f.endswith('.py') or isdir) |
|
|
|
for t in installdata.targets: |
|
if should_append(t.out_name): |
|
py_files.append((t.out_name, os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname)))) |
|
for d in installdata.data: |
|
if should_append(d.install_path_name): |
|
py_files.append((d.install_path_name, os.path.join(installdata.prefix, d.install_path))) |
|
for d in installdata.install_subdirs: |
|
if should_append(d.install_path_name, True): |
|
py_files.append((d.install_path_name, os.path.join(installdata.prefix, d.install_path))) |
|
|
|
import importlib.resources |
|
pycompile = os.path.join(self.interpreter.environment.get_scratch_dir(), 'pycompile.py') |
|
with open(pycompile, 'wb') as f: |
|
f.write(importlib.resources.read_binary('mesonbuild.scripts', 'pycompile.py')) |
|
|
|
for i in self.installations.values(): |
|
if isinstance(i, PythonExternalProgram) and i.run_bytecompile[i.info['version']]: |
|
i = T.cast(PythonExternalProgram, i) |
|
manifest = f'python-{i.info["version"]}-installed.json' |
|
manifest_json = [] |
|
for name, f in py_files: |
|
if f.startswith((os.path.join(installdata.prefix, i.platlib), os.path.join(installdata.prefix, i.purelib))): |
|
manifest_json.append(name) |
|
with open(os.path.join(self.interpreter.environment.get_scratch_dir(), manifest), 'w', encoding='utf-8') as f: |
|
json.dump(manifest_json, f) |
|
cmd = i.command + [pycompile, manifest, str(optlevel)] |
|
|
|
script = backend.get_executable_serialisation(cmd, verbose=True, tag='python-runtime', |
|
installdir_map={'py_purelib': i.purelib, 'py_platlib': i.platlib}) |
|
ret.append(script) |
|
return ret |
|
|
|
def postconf_hook(self, b: Build) -> None: |
|
b.install_scripts.extend(self._get_install_scripts()) |
|
|
|
# https://www.python.org/dev/peps/pep-0397/ |
|
@staticmethod |
|
def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]: |
|
if not name_or_path.startswith(('python2', 'python3')): |
|
return None |
|
if not shutil.which('py'): |
|
# program not installed, return without an exception |
|
return None |
|
ver = f'-{name_or_path[6:]}' |
|
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 _find_installation_impl(self, state: 'ModuleState', display_name: str, name_or_path: str, required: bool) -> ExternalProgram: |
|
if not name_or_path: |
|
python = PythonExternalProgram('python3', mesonlib.python_command) |
|
else: |
|
tmp_python = ExternalProgram.from_entry(display_name, name_or_path) |
|
python = PythonExternalProgram(display_name, 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'}: |
|
tmp_python = ExternalProgram.from_entry(display_name, 'python') |
|
python = PythonExternalProgram(name_or_path, ext_prog=tmp_python) |
|
|
|
if python.found(): |
|
if python.sanity(state): |
|
return python |
|
else: |
|
sanitymsg = f'{python} is not a valid python or it is missing distutils' |
|
if required: |
|
raise mesonlib.MesonException(sanitymsg) |
|
else: |
|
mlog.warning(sanitymsg, location=state.current_node) |
|
|
|
return NonExistingExternalProgram(python.name) |
|
|
|
@disablerIfNotFound |
|
@typed_pos_args('python.find_installation', optargs=[str]) |
|
@typed_kwargs( |
|
'python.find_installation', |
|
KwargInfo('required', (bool, UserFeatureOption), default=True), |
|
KwargInfo('disabler', bool, default=False, since='0.49.0'), |
|
KwargInfo('modules', ContainerTypeInfo(list, str), listify=True, default=[], since='0.51.0'), |
|
_PURE_KW.evolve(default=True, since='0.64.0'), |
|
) |
|
def find_installation(self, state: 'ModuleState', args: T.Tuple[T.Optional[str]], |
|
kwargs: 'FindInstallationKw') -> ExternalProgram: |
|
feature_check = FeatureNew('Passing "feature" option to find_installation', '0.48.0') |
|
disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, feature_check) |
|
|
|
# FIXME: this code is *full* of sharp corners. It assumes that it's |
|
# going to get a string value (or now a list of length 1), of `python2` |
|
# or `python3` which is completely nonsense. On windows the value could |
|
# easily be `['py', '-3']`, or `['py', '-3.7']` to get a very specific |
|
# version of python. On Linux we might want a python that's not in |
|
# $PATH, or that uses a wrapper of some kind. |
|
np: T.List[str] = state.environment.lookup_binary_entry(MachineChoice.HOST, 'python') or [] |
|
fallback = args[0] |
|
display_name = fallback or 'python' |
|
if not np and fallback is not None: |
|
np = [fallback] |
|
name_or_path = np[0] if np else None |
|
|
|
if disabled: |
|
mlog.log('Program', name_or_path or 'python', 'found:', mlog.red('NO'), '(disabled by:', mlog.bold(feature), ')') |
|
return NonExistingExternalProgram() |
|
|
|
python = self.installations.get(name_or_path) |
|
if not python: |
|
python = self._find_installation_impl(state, display_name, name_or_path, required) |
|
self.installations[name_or_path] = python |
|
|
|
want_modules = kwargs['modules'] |
|
found_modules: T.List[str] = [] |
|
missing_modules: T.List[str] = [] |
|
if python.found() and want_modules: |
|
for mod in want_modules: |
|
p, *_ = mesonlib.Popen_safe( |
|
python.command + |
|
['-c', f'import {mod}']) |
|
if p.returncode != 0: |
|
missing_modules.append(mod) |
|
else: |
|
found_modules.append(mod) |
|
|
|
msg: T.List['mlog.TV_Loggable'] = ['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(python.name) |
|
elif missing_modules: |
|
if required: |
|
raise mesonlib.MesonException('{} is missing modules: {}'.format(name_or_path or 'python', ', '.join(missing_modules))) |
|
return NonExistingExternalProgram(python.name) |
|
else: |
|
python = copy.copy(python) |
|
python.pure = kwargs['pure'] |
|
python.run_bytecompile.setdefault(python.info['version'], False) |
|
return python |
|
|
|
raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).') |
|
|
|
|
|
def initialize(interpreter: 'Interpreter') -> PythonModule: |
|
mod = PythonModule(interpreter) |
|
mod.interpreter.append_holder_map(PythonExternalProgram, PythonInstallation) |
|
return mod
|
|
|