Skip to content

Commit a211c3f

Browse files
committed
python: add a python.build_config option (PEP 739)
Signed-off-by: Filipe Laíns <[email protected]>
1 parent d16d539 commit a211c3f

File tree

4 files changed

+112
-35
lines changed

4 files changed

+112
-35
lines changed

docs/markdown/Builtin-options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ install prefix. For example: if the install prefix is `/usr` and the
413413
| platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) |
414414
| purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) |
415415
| allow_limited_api | true | true, false | Disables project-wide use of the Python Limited API (Since 1.3.0) |
416+
| build_config | | File path | Specifies the Python build configuration file (PEP 739) (Since 1.7.0) |
416417

417418
*Since 0.60.0* The `python.platlibdir` and `python.purelibdir` options are used
418419
by the python module methods `python.install_sources()` and

mesonbuild/dependencies/python.py

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.An
7676

7777
class BasicPythonExternalProgram(ExternalProgram):
7878
def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
79-
ext_prog: T.Optional[ExternalProgram] = None):
79+
ext_prog: T.Optional[ExternalProgram] = None,
80+
build_config_path: T.Optional[str] = None):
8081
if ext_prog is None:
8182
super().__init__(name, command=command, silent=True)
8283
else:
@@ -86,6 +87,15 @@ def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
8687
self.cached_version = None
8788
self.version_arg = '--version'
8889

90+
self.build_config = None
91+
92+
if build_config_path:
93+
try:
94+
with open(build_config_path, encoding='utf8') as f:
95+
self.build_config = json.load(f)
96+
except OSError as e:
97+
raise DependencyException(f'Failed to read python.build_config: {e}') from e
98+
8999
# We want strong key values, so we always populate this with bogus data.
90100
# Otherwise to make the type checkers happy we'd have to do .get() for
91101
# everycall, even though we know that the introspection data will be
@@ -116,6 +126,14 @@ def _check_version(self, version: str) -> bool:
116126
def sanity(self) -> bool:
117127
# Sanity check, we expect to have something that at least quacks in tune
118128

129+
if self.build_config:
130+
if not self.build_config['libpython']:
131+
mlog.debug('This Python installation does not provide a libpython')
132+
return False
133+
if not self.build_config['c_api']:
134+
mlog.debug('This Python installation does support the C API')
135+
return False
136+
119137
import importlib.resources
120138

121139
with importlib.resources.path('mesonbuild.scripts', 'python_info.py') as f:
@@ -145,12 +163,24 @@ class _PythonDependencyBase(_Base):
145163

146164
def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool):
147165
self.embed = embed
148-
self.version: str = python_holder.info['version']
149-
self.platform = python_holder.info['platform']
150-
self.variables = python_holder.info['variables']
166+
self.build_config = python_holder.build_config
167+
168+
if self.build_config:
169+
self.version = self.build_config['language']['version']
170+
self.platform = self.build_config['platform']
171+
self.is_freethreaded = 't' in self.build_config['abi']['flags']
172+
self.link_libpython = self.build_config['libpython']['link_extensions']
173+
else:
174+
self.version = python_holder.info['version']
175+
self.platform = python_holder.info['platform']
176+
self.is_freethreaded = python_holder.info['is_freethreaded']
177+
self.link_libpython = python_holder.info['link_libpython']
178+
# This data shouldn't be needed when build_config is set
179+
self.is_pypy = python_holder.info['is_pypy']
180+
self.variables = python_holder.info['variables']
181+
151182
self.paths = python_holder.info['paths']
152-
self.is_pypy = python_holder.info['is_pypy']
153-
self.is_freethreaded = python_holder.info['is_freethreaded']
183+
154184
# The "-embed" version of python.pc / python-config was introduced in 3.8,
155185
# and distutils extension linking was changed to be considered a non embed
156186
# usage. Before then, this dependency always uses the embed=True handling
@@ -159,7 +189,9 @@ def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool):
159189
# On macOS and some Linux distros (Debian) distutils doesn't link extensions
160190
# against libpython, even on 3.7 and below. We call into distutils and
161191
# mirror its behavior. See https://github.com/mesonbuild/meson/issues/4117
162-
self.link_libpython = python_holder.info['link_libpython'] or embed
192+
if not self.link_libpython:
193+
self.link_libpython = embed
194+
163195
self.info: T.Optional[T.Dict[str, str]] = None
164196
if mesonlib.version_compare(self.version, '>= 3.0'):
165197
self.major_version = 3
@@ -173,20 +205,27 @@ def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool):
173205
self.compile_args += ['-DPy_GIL_DISABLED']
174206

175207
def find_libpy(self, environment: 'Environment') -> None:
176-
if self.is_pypy:
177-
if self.major_version == 3:
178-
libname = 'pypy3-c'
179-
else:
180-
libname = 'pypy-c'
181-
libdir = os.path.join(self.variables.get('base'), 'bin')
182-
libdirs = [libdir]
208+
if self.build_config:
209+
path = self.build_config['libpython'].get('dynamic')
210+
if not path:
211+
raise DependencyException('Python does not provide a dynamic libpython library')
212+
libdirs = [os.path.dirname(path)]
213+
libname = os.path.basename(path)
183214
else:
184-
libname = f'python{self.version}'
185-
if 'DEBUG_EXT' in self.variables:
186-
libname += self.variables['DEBUG_EXT']
187-
if 'ABIFLAGS' in self.variables:
188-
libname += self.variables['ABIFLAGS']
189-
libdirs = []
215+
if self.is_pypy:
216+
if self.major_version == 3:
217+
libname = 'pypy3-c'
218+
else:
219+
libname = 'pypy-c'
220+
libdir = os.path.join(self.variables.get('base'), 'bin')
221+
libdirs = [libdir]
222+
else:
223+
libname = f'python{self.version}'
224+
if 'DEBUG_EXT' in self.variables:
225+
libname += self.variables['DEBUG_EXT']
226+
if 'ABIFLAGS' in self.variables:
227+
libname += self.variables['ABIFLAGS']
228+
libdirs = []
190229

191230
largs = self.clib_compiler.find_library(libname, environment, libdirs)
192231
if largs is not None:
@@ -212,6 +251,15 @@ def get_windows_python_arch(self) -> str:
212251
raise DependencyException('Unknown Windows Python platform {self.platform!r}')
213252

214253
def get_windows_link_args(self, limited_api: bool) -> T.Optional[T.List[str]]:
254+
if self.build_config:
255+
if self.static:
256+
key = 'static'
257+
elif limited_api:
258+
key = 'dynamic-stableabi'
259+
else:
260+
key = 'dynamic'
261+
return [self.build_config['libpython'][key]]
262+
215263
if self.platform.startswith('win'):
216264
vernum = self.variables.get('py_version_nodot')
217265
verdot = self.variables.get('py_version_short')
@@ -360,10 +408,13 @@ def __init__(self, name: str, environment: 'Environment',
360408
self.is_found = True
361409

362410
# compile args
363-
inc_paths = mesonlib.OrderedSet([
364-
self.variables.get('INCLUDEPY'),
365-
self.paths.get('include'),
366-
self.paths.get('platinclude')])
411+
if self.build_config:
412+
inc_paths = mesonlib.OrderedSet([self.build_config['c_api']['headers']])
413+
else:
414+
inc_paths = mesonlib.OrderedSet([
415+
self.variables.get('INCLUDEPY'),
416+
self.paths.get('include'),
417+
self.paths.get('platinclude')])
367418

368419
self.compile_args += ['-I' + path for path in inc_paths if path]
369420

@@ -392,11 +443,18 @@ def python_factory(env: 'Environment', for_machine: 'MachineChoice',
392443
if installation is None:
393444
installation = BasicPythonExternalProgram('python3', mesonlib.python_command)
394445
installation.sanity()
395-
pkg_version = installation.info['variables'].get('LDVERSION') or installation.info['version']
446+
447+
if installation.build_config:
448+
pkg_version = installation.build_config['language']['version']
449+
else:
450+
pkg_version = installation.info['variables'].get('LDVERSION') or installation.info['version']
396451

397452
if DependencyMethods.PKGCONFIG in methods:
398453
if from_installation:
399-
pkg_libdir = installation.info['variables'].get('LIBPC')
454+
if installation.build_config:
455+
pkg_libdir = installation.build_config['c_api']['pkgconfig_path']
456+
else:
457+
pkg_libdir = installation.info['variables'].get('LIBPC')
400458
pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.info['version'], '>=3.8') else ''
401459
pkg_name = f'python-{pkg_version}{pkg_embed}'
402460

mesonbuild/modules/python.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,27 @@ def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'):
115115
info = python.info
116116
prefix = self.interpreter.environment.coredata.get_option(OptionKey('prefix'))
117117
assert isinstance(prefix, str), 'for mypy'
118+
119+
if python.build_config:
120+
self.version = python.build_config['language']['version']
121+
self.platform = python.build_config['platform']
122+
self.suffix = python.build_config['abi']['extension_suffix']
123+
self.limited_api_suffix = python.build_config['abi']['stable_abi_suffix']
124+
self.link_libpython = python.build_config['libpython']['link_extensions']
125+
self.is_pypy = python.build_config['implementation']['name'] == 'pypy'
126+
else:
127+
self.version = info['version']
128+
self.platform = info['platform']
129+
self.suffix = info['suffix']
130+
self.limited_api_suffix = info['limited_api_suffix']
131+
self.link_libpython = info['link_libpython']
132+
self.is_pypy = info['is_pypy']
133+
118134
self.variables = info['variables']
119-
self.suffix = info['suffix']
120-
self.limited_api_suffix = info['limited_api_suffix']
121135
self.paths = info['paths']
122136
self.pure = python.pure
123137
self.platlib_install_path = os.path.join(prefix, python.platlib)
124138
self.purelib_install_path = os.path.join(prefix, python.purelib)
125-
self.version = info['version']
126-
self.platform = info['platform']
127-
self.is_pypy = info['is_pypy']
128-
self.link_libpython = info['link_libpython']
129139
self.methods.update({
130140
'extension_module': self.extension_module_method,
131141
'dependency': self.dependency_method,
@@ -256,8 +266,12 @@ def _dependency_method_impl(self, kwargs: TYPE_kwargs) -> Dependency:
256266
if dep is not None:
257267
return dep
258268

269+
build_config = self.interpreter.environment.coredata.get_option(OptionKey('python.build_config'))
270+
259271
new_kwargs = kwargs.copy()
260272
new_kwargs['required'] = False
273+
if build_config:
274+
new_kwargs['build_config'] = build_config
261275
candidates = python_factory(self.interpreter.environment, for_machine, new_kwargs, self.held_object)
262276
dep = find_external_dependency('python', self.interpreter.environment, new_kwargs, candidates)
263277

@@ -441,11 +455,13 @@ def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]:
441455
return None
442456

443457
def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_or_path: str, required: bool) -> MaybePythonProg:
458+
build_config = self.interpreter.environment.coredata.get_option(OptionKey('python.build_config'))
459+
444460
if not name_or_path:
445-
python = PythonExternalProgram('python3', mesonlib.python_command)
461+
python = PythonExternalProgram('python3', mesonlib.python_command, build_config_path=build_config)
446462
else:
447463
tmp_python = ExternalProgram.from_entry(display_name, name_or_path)
448-
python = PythonExternalProgram(display_name, ext_prog=tmp_python)
464+
python = PythonExternalProgram(display_name, ext_prog=tmp_python, build_config_path=build_config)
449465

450466
if not python.found() and mesonlib.is_windows():
451467
pythonpath = self._get_win_pythonpath(name_or_path)
@@ -459,7 +475,7 @@ def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_
459475
# it
460476
if not python.found() and name_or_path in {'python2', 'python3'}:
461477
tmp_python = ExternalProgram.from_entry(display_name, 'python')
462-
python = PythonExternalProgram(name_or_path, ext_prog=tmp_python)
478+
python = PythonExternalProgram(name_or_path, ext_prog=tmp_python, build_config_path=build_config)
463479

464480
if python.found():
465481
if python.sanity(state):

mesonbuild/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,8 @@ def add_to_argparse(self, name: OptionKey, parser: argparse.ArgumentParser, help
775775
BuiltinOption(UserStringOption, 'Directory for site-specific, non-platform-specific files.', '')),
776776
(OptionKey('python.allow_limited_api'),
777777
BuiltinOption(UserBooleanOption, 'Whether to allow use of the Python Limited API', True)),
778+
(OptionKey('python.build_config'),
779+
BuiltinOption(UserStringOption, 'Config file containing the build details for the target Python installation.', '')),
778780
])
779781

780782
BUILTIN_OPTIONS = OrderedDict(chain(BUILTIN_DIR_OPTIONS.items(), BUILTIN_CORE_OPTIONS.items()))

0 commit comments

Comments
 (0)