Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 70 additions & 65 deletions mesonbuild/dependencies/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,69 +161,6 @@ def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool):
else:
self.major_version = 2


class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase):

def __init__(self, name: str, environment: 'Environment',
kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram',
libpc: bool = False):
if libpc:
mlog.debug(f'Searching for {name!r} via pkgconfig lookup in LIBPC')
else:
mlog.debug(f'Searching for {name!r} via fallback pkgconfig lookup in default paths')

PkgConfigDependency.__init__(self, name, environment, kwargs)
_PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))

if libpc and not self.is_found:
mlog.debug(f'"python-{self.version}" could not be found in LIBPC, this is likely due to a relocated python installation')

# pkg-config files are usually accurate starting with python 3.8
if not self.link_libpython and mesonlib.version_compare(self.version, '< 3.8'):
self.link_args = []


class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase):

def __init__(self, name: str, environment: 'Environment',
kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram'):
ExtraFrameworkDependency.__init__(self, name, environment, kwargs)
_PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))


class PythonSystemDependency(SystemDependency, _PythonDependencyBase):

def __init__(self, name: str, environment: 'Environment',
kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram'):
SystemDependency.__init__(self, name, environment, kwargs)
_PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))

# match pkg-config behavior
if self.link_libpython:
# link args
if mesonlib.is_windows():
self.find_libpy_windows(environment, limited_api=False)
else:
self.find_libpy(environment)
else:
self.is_found = True

# 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/
# https://github.com/python/cpython/pull/100137
if mesonlib.is_windows() and self.get_windows_python_arch().endswith('64') and mesonlib.version_compare(self.version, '<3.12'):
self.compile_args += ['-DMS_WIN64=']

if not self.clib_compiler.has_header('Python.h', '', environment, extra_args=self.compile_args)[0]:
self.is_found = False

def find_libpy(self, environment: 'Environment') -> None:
if self.is_pypy:
if self.major_version == 3:
Expand Down Expand Up @@ -311,9 +248,15 @@ def get_windows_link_args(self, limited_api: bool) -> T.Optional[T.List[str]]:
lib = Path(self.variables.get('base_prefix')) / libpath
elif self.platform.startswith('mingw'):
if self.static:
libname = self.variables.get('LIBRARY')
if limited_api:
libname = self.variables.get('ABI3DLLLIBRARY')
else:
libname = self.variables.get('LIBRARY')
else:
libname = self.variables.get('LDLIBRARY')
if limited_api:
libname = self.variables.get('ABI3LDLIBRARY')
else:
libname = self.variables.get('LDLIBRARY')
lib = Path(self.variables.get('LIBDIR')) / libname
else:
raise mesonlib.MesonBugException(
Expand Down Expand Up @@ -347,6 +290,68 @@ def find_libpy_windows(self, env: 'Environment', limited_api: bool = False) -> N
self.link_args = largs
self.is_found = True

class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be missing something, but the only thing this patch does is to move the code around unmodified in the same file. What's the point? Why does the code need to be moved?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change to the Windows checking logic introduced in 663700c means that a PythonPkgConfigDependency might now be found where previously only a PythonSystemDependency was found.

The find_libpy_windows function previously only existed on PythonSystemDependency.

Moving this function (and all the functions it calls transitively) into the base class seemed like the easiest solution to the AttributeErrors that I was seeing during the initial development of this patch. It is a bit brutal though, I admit.

The change in 663700c is not enough to resolve the initial issue that this PR addresses. Ultimately the change in b26f306 is necessary in order to find the right library under mingw.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff is deceptive, since a giant block of code gets moved around but it looks like a bunch of classes got moved around some functions, but what actually happened is some functions got moved from one class to another.

It's not easy to tell that the moved code starts halfway through a class definition (thus indicating that the semantics changed).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to stare at it some more to get it. It is deceptive indeed that the moved functions are the ones not appearing in the diff. Now I get it.


def __init__(self, name: str, environment: 'Environment',
kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram',
libpc: bool = False):
if libpc:
mlog.debug(f'Searching for {name!r} via pkgconfig lookup in LIBPC')
else:
mlog.debug(f'Searching for {name!r} via fallback pkgconfig lookup in default paths')

PkgConfigDependency.__init__(self, name, environment, kwargs)
_PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))

if libpc and not self.is_found:
mlog.debug(f'"python-{self.version}" could not be found in LIBPC, this is likely due to a relocated python installation')

# pkg-config files are usually accurate starting with python 3.8
if not self.link_libpython and mesonlib.version_compare(self.version, '< 3.8'):
self.link_args = []


class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase):

def __init__(self, name: str, environment: 'Environment',
kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram'):
ExtraFrameworkDependency.__init__(self, name, environment, kwargs)
_PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))


class PythonSystemDependency(SystemDependency, _PythonDependencyBase):

def __init__(self, name: str, environment: 'Environment',
kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram'):
SystemDependency.__init__(self, name, environment, kwargs)
_PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False))

# match pkg-config behavior
if self.link_libpython:
# link args
if mesonlib.is_windows():
self.find_libpy_windows(environment, limited_api=False)
else:
self.find_libpy(environment)
else:
self.is_found = True

# 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/
# https://github.com/python/cpython/pull/100137
if mesonlib.is_windows() and self.get_windows_python_arch().endswith('64') and mesonlib.version_compare(self.version, '<3.12'):
self.compile_args += ['-DMS_WIN64=']

if not self.clib_compiler.has_header('Python.h', '', environment, extra_args=self.compile_args)[0]:
self.is_found = False

@staticmethod
def log_tried() -> str:
return 'sysconfig'
Expand Down
14 changes: 8 additions & 6 deletions mesonbuild/modules/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,9 @@ def extension_module_method(self, args: T.Tuple[str, T.List[BuildTargetSource]],
new_cpp_args.append(limited_api_definition)
kwargs['cpp_args'] = new_cpp_args

# When compiled under MSVC, Python's PC/pyconfig.h forcibly inserts pythonMAJOR.MINOR.lib
# into the linker path when not running in debug mode via a series #pragma comment(lib, "")
# directives. We manually override these here as this interferes with the intended
# use of the 'limited_api' kwarg
# On Windows, the limited API DLL is python3.dll, not python3X.dll.
for_machine = kwargs['native']
compilers = self.interpreter.environment.coredata.compilers[for_machine]
if any(compiler.get_id() == 'msvc' for compiler in compilers.values()):
if self.interpreter.environment.machines[for_machine].is_windows():
pydep_copy = copy.copy(pydep)
pydep_copy.find_libpy_windows(self.env, limited_api=True)
if not pydep_copy.found():
Expand All @@ -199,6 +195,12 @@ def extension_module_method(self, args: T.Tuple[str, T.List[BuildTargetSource]],
new_deps.remove(pydep)
new_deps.append(pydep_copy)

# When compiled under MSVC, Python's PC/pyconfig.h forcibly inserts pythonMAJOR.MINOR.lib
# into the linker path when not running in debug mode via a series #pragma comment(lib, "")
# directives. We manually override these here as this interferes with the intended
# use of the 'limited_api' kwarg
compilers = self.interpreter.environment.coredata.compilers[for_machine]
if any(compiler.get_id() == 'msvc' for compiler in compilers.values()):
pyver = pydep.version.replace('.', '')
python_windows_debug_link_exception = f'/NODEFAULTLIB:python{pyver}_d.lib'
python_windows_release_link_exception = f'/NODEFAULTLIB:python{pyver}.lib'
Expand Down
14 changes: 12 additions & 2 deletions test cases/python/9 extmodule limited api/limited.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@
#error Wrong value for Py_LIMITED_API
#endif

static PyObject *
hello(PyObject * Py_UNUSED(self), PyObject * Py_UNUSED(args)) {
return PyUnicode_FromString("hello world");
}

static struct PyMethodDef methods[] = {
{ "hello", hello, METH_NOARGS, NULL },
{ NULL, NULL, 0, NULL },
};

static struct PyModuleDef limited_module = {
PyModuleDef_HEAD_INIT,
"limited_api_test",
"limited",
NULL,
-1,
NULL
methods
};

PyMODINIT_FUNC PyInit_limited(void) {
Expand Down
7 changes: 7 additions & 0 deletions test cases/python/9 extmodule limited api/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ ext_mod = py.extension_module('not_limited',
'not_limited.c',
install: true,
)

test('load-test',
py,
args: [files('test_limited.py')],
env: { 'PYTHONPATH': meson.current_build_dir() },
workdir: meson.current_source_dir()
)
6 changes: 6 additions & 0 deletions test cases/python/9 extmodule limited api/test_limited.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from limited import hello

def test_hello():
assert hello() == "hello world"

test_hello()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be condensed to be just

from limited import hello
assert hello() == 'hello world'

31 changes: 30 additions & 1 deletion unittests/pythontests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .baseplatformtests import BasePlatformTests
from .helpers import *

from mesonbuild.mesonlib import MachineChoice, TemporaryDirectoryWinProof
from mesonbuild.mesonlib import MachineChoice, TemporaryDirectoryWinProof, is_windows
from mesonbuild.modules.python import PythonModule

class PythonTests(BasePlatformTests):
Expand Down Expand Up @@ -86,3 +86,32 @@ def test_bytecompile_single(self):
if shutil.which('python2') or PythonModule._get_win_pythonpath('python2'):
raise self.skipTest('python2 installed, already tested')
self._test_bytecompile()

def test_limited_api_linked_correct_lib(self):
if not is_windows():
return self.skipTest('Test only run on Windows.')

testdir = os.path.join(self.src_root, 'test cases', 'python', '9 extmodule limited api')

self.init(testdir)
self.build()

from importlib.machinery import EXTENSION_SUFFIXES
limited_suffix = EXTENSION_SUFFIXES[1]

limited_library_path = os.path.join(self.builddir, f'limited{limited_suffix}')
self.assertPathExists(limited_library_path)

limited_dep_name = 'python3.dll'
if shutil.which('dumpbin'):
# MSVC
output = subprocess.check_output(['dumpbin', '/DEPENDENTS', limited_library_path],
stderr=subprocess.STDOUT)
self.assertIn(limited_dep_name, output.decode())
elif shutil.which('objdump'):
# mingw
output = subprocess.check_output(['objdump', '-p', limited_library_path],
stderr=subprocess.STDOUT)
Comment on lines +108 to +114
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to look in stderr as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly out of paranoia. When I was implementing this it was handy to have both stderr and stdout so that when it didn't work correctly, the contents of both stdout and stderr ended up in the log. Should this be removed?

self.assertIn(limited_dep_name, output.decode())
else:
raise self.skipTest('Test needs either dumpbin(MSVC) or objdump(mingw).')