Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modules/python: detect debian distutils missing and show an error #9335

Closed
wants to merge 1 commit into from
Closed
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
94 changes: 60 additions & 34 deletions mesonbuild/modules/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,19 @@ def set_env(name, value):
import sysconfig
import json
import sys
import distutils.command.install

def debian_distutils_missing():
# Debian partially splits the distutils module from the python3, they
# keep distutils and distutils.version in python3 and put the rest of
# the submodules in python3-distutils
try:
import distutils.version
FFY00 marked this conversation as resolved.
Show resolved Hide resolved
import distutils.core
except ModuleNotFoundError as e:
if e.name == 'distutils.core':
# distutils.version was successfully imported, but distutils.core was not
return True
return False

def get_distutils_paths(scheme=None, prefix=None):
import distutils.dist
Expand All @@ -297,37 +309,47 @@ def get_distutils_paths(scheme=None, prefix=None):
'scripts': install_cmd.install_scripts,
}

# On Debian derivatives, the Python interpreter shipped by the distribution uses
# a custom install scheme, deb_system, for the system install, and changes the
# default scheme to a custom one pointing to /usr/local and replacing
# site-packages with dist-packages.
# See https://github.com/mesonbuild/meson/issues/8739.
# XXX: We should be using sysconfig, but Debian only patches distutils.

if 'deb_system' in distutils.command.install.INSTALL_SCHEMES:
paths = get_distutils_paths(scheme='deb_system')
install_paths = get_distutils_paths(scheme='deb_system', prefix='')
else:
paths = sysconfig.get_paths()
empty_vars = {'base': '', 'platbase': '', 'installed_base': ''}
install_paths = sysconfig.get_paths(vars=empty_vars)
def get_paths():
# On Debian derivatives, the Python interpreter shipped by the distribution uses
# a custom install scheme, deb_system, for the system install, and changes the
# default scheme to a custom one pointing to /usr/local and replacing
# site-packages with dist-packages.
# See https://github.com/mesonbuild/meson/issues/8739.
# XXX: We should be using sysconfig, but Debian only patches distutils.
import distutils.command.install
if 'deb_system' in distutils.command.install.INSTALL_SCHEMES:
paths = get_distutils_paths(scheme='deb_system')
install_paths = get_distutils_paths(scheme='deb_system', prefix='')
else:
paths = sysconfig.get_paths()
empty_vars = {'base': '', 'platbase': '', 'installed_base': ''}
install_paths = sysconfig.get_paths(vars=empty_vars)
return paths, install_paths

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': paths,
'install_paths': install_paths,
'sys_paths': sys.path,
'version': sysconfig.get_python_version(),
'platform': sysconfig.get_platform(),
'is_pypy': '__pypy__' in sys.builtin_module_names,
'link_libpython': links_against_libpython(),
}))
if debian_distutils_missing():
data = {
'error': 'Debian distutils is missing, please install python3-distutils',
}
else:
paths, install_paths = get_paths()
data = {
'variables': sysconfig.get_config_vars(),
'paths': paths,
'install_paths': install_paths,
'sys_paths': sys.path,
'version': sysconfig.get_python_version(),
'platform': sysconfig.get_platform(),
'is_pypy': '__pypy__' in sys.builtin_module_names,
'link_libpython': links_against_libpython(),
}

print(json.dumps(data))
'''

if T.TYPE_CHECKING:
Expand Down Expand Up @@ -388,15 +410,19 @@ def sanity(self, state: T.Optional['ModuleState'] = None) -> bool:
mlog.debug('Program stderr:\n')
mlog.debug(stderr)

if info is not None and self._check_version(info['version']):
variables = info['variables']
info['suffix'] = variables.get('EXT_SUFFIX') or variables.get('SO') or variables.get('.so')
self.info = T.cast('PythonIntrospectionDict', info)
self.platlib = self._get_path(state, 'platlib')
self.purelib = self._get_path(state, 'purelib')
return True
else:
return False
if info is not None:
if 'error' in info:
assert isinstance(info['error'], str)
mlog.log('Python interpreter introspection failed: {}'.format(info['error']))
return False
elif self._check_version(info['version']):
variables = info['variables']
info['suffix'] = variables.get('EXT_SUFFIX') or variables.get('SO') or variables.get('.so')
self.info = T.cast('PythonIntrospectionDict', info)
self.platlib = self._get_path(state, 'platlib')
self.purelib = self._get_path(state, 'purelib')
return True
return False

def _get_path(self, state: T.Optional['ModuleState'], key: str) -> None:
if state:
Expand Down
10 changes: 10 additions & 0 deletions unittests/pythontests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

import os
import unittest
import unittest.mock

from run_tests import (
Backend
)

from mesonbuild.modules.python import PythonExternalProgram
from .baseplatformtests import BasePlatformTests

class PythonTests(BasePlatformTests):
Expand Down Expand Up @@ -80,3 +82,11 @@ def test_versions(self):
with self.assertRaises(unittest.SkipTest):
self.init(testdir, extra_args=['-Dpython=dir'])
self.wipe()

@unittest.mock.patch('mesonbuild.mesonlib.Popen_safe', return_value=(None, '{"error": "hello!"}', None))
@unittest.mock.patch('mesonbuild.mlog.log')
def test_introspection_error(self, mlog_log, Popen_safe):
python = PythonExternalProgram('some-python', ['some-python'])

assert not python.sanity()
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, shouldn't this be a unittest assertFalse, not a regular assert?

Copy link
Contributor

@cclauss cclauss Oct 8, 2021

Choose a reason for hiding this comment

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

When tests are run with pytest, either is acceptable. In fact, this simplicity without loss of debugging information is pytest's number one feature.

Copy link
Member

Choose a reason for hiding this comment

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

Well yes, but if you don't have both pytest and pytest-xdist then the tests get run without pytest, just unittest, and that is a supported configuration...

Copy link
Member Author

Choose a reason for hiding this comment

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

It's the same thing in this case, these functions just exist to provide a nicer error output. pytest rewrites the bytecode to replace the asserts with something similar in order to achieve that. It makes sense when you are comparing lists for eg, but as we are just checking a bool, is pretty much the same.

Copy link
Member

Choose a reason for hiding this comment

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

I know what pytest does, thanks.

Again, my point is that we don't depend on pytest (even if we do automatically use it some of the time).

Copy link
Member

Choose a reason for hiding this comment

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

If it's "pretty much the same thing" to you, it shouldn't be a problem to use unittest asserts for the sake of those odd people using unittest instead of pytest to run this testsuite :)

Copy link
Member Author

Choose a reason for hiding this comment

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

It's pretty much the same thing because we are just checking a bool, which does not show any extra information, unlike comparing lists for eg.
I just used an assert because that's what I am used to, but I can change it if you want. Though I am not home right now, and have an appointment afterwards, so it might be a couple hours.

mlog_log.assert_called_with('Python interpreter introspection failed: hello!')