Repository: mesonbuild/meson

Issue:
Boost Python library detection is incompatible with Boost 1.67 and later
In Boost 1.67, Boost changed their naming scheme for the shared libraries they compile for Boost Python. All libraries are now named according to the Python interpreter they were compiled against. From the [Boost 1.67.0 changelog](https://www.boost.org/users/history/version_1_67_0.html):

```
The library name now includes the version suffix of the Python version used to compile it. For example, a variant compiled with Python 2.7 will produce library names boost_python27 and boost_numpy27, etc.. Combined with a related fix in Boost.Build, this means that it is now possible to build variants for multiple Python versions in a single build process. 
```

This breaks Meson's Boost Python detection since it looks for a shared library [named just boost_python with no suffix](https://github.com/mesonbuild/meson/blob/master/mesonbuild/dependencies/boost.py#L550).


Discussion:

Resolving patch:
diff --git a/mesonbuild/dependencies/boost.py b/mesonbuild/dependencies/boost.py
--- a/mesonbuild/dependencies/boost.py
+++ b/mesonbuild/dependencies/boost.py
@@ -134,22 +134,31 @@ def __init__(self, environment, kwargs):
             else:
                 self.incdir = self.detect_nix_incdir()
 
-        if self.check_invalid_modules():
-            return
-
         mlog.debug('Boost library root dir is', mlog.bold(self.boost_root))
         mlog.debug('Boost include directory is', mlog.bold(self.incdir))
 
         # 1. check if we can find BOOST headers.
         self.detect_headers_and_version()
 
+        if not self.is_found:
+            return # if we can not find 'boost/version.hpp'
+
         # 2. check if we can find BOOST libraries.
-        if self.is_found:
-            self.detect_lib_modules()
-            mlog.debug('Boost library directory is', mlog.bold(self.libdir))
+        self.detect_lib_modules()
+        mlog.debug('Boost library directory is', mlog.bold(self.libdir))
+
+        mlog.debug('Installed Boost libraries: ')
+        for key in sorted(self.lib_modules.keys()):
+            mlog.debug(key, self.lib_modules[key])
+
+        # 3. check if requested modules are valid, that is, either found or in the list of known boost libraries
+        self.check_invalid_modules()
+
+        # 4. final check whether or not we find all requested and valid modules
+        self.check_find_requested_modules()
 
     def check_invalid_modules(self):
-        invalid_modules = [c for c in self.requested_modules if 'boost_' + c not in BOOST_LIBS]
+        invalid_modules = [c for c in self.requested_modules if 'boost_' + c not in self.lib_modules and 'boost_' + c not in BOOST_LIBS]
 
         # previous versions of meson allowed include dirs as modules
         remove = []
@@ -273,6 +282,7 @@ def detect_lib_modules(self):
             else:
                 self.detect_lib_modules_nix()
 
+    def check_find_requested_modules(self):
         # 3. Check if we can find the modules
         for m in self.requested_modules:
             if 'boost_' + m not in self.lib_modules:
@@ -491,7 +501,6 @@ def get_link_args(self, **kwargs):
     def get_sources(self):
         return []
 
-
 # Generated with boost_names.py
 BOOST_LIBS = [
     'boost_atomic',
@@ -547,10 +556,6 @@ def get_sources(self):
     'boost_math_c99l',
     'boost_mpi',
     'boost_program_options',
-    'boost_python',
-    'boost_python3',
-    'boost_numpy',
-    'boost_numpy3',
     'boost_random',
     'boost_regex',
     'boost_serialization',


Test patch:
diff --git a/test cases/frameworks/1 boost/meson.build b/test cases/frameworks/1 boost/meson.build
--- a/test cases/frameworks/1 boost/meson.build	
+++ b/test cases/frameworks/1 boost/meson.build	
@@ -26,18 +26,71 @@ testdep = dependency('boost', modules : ['unit_test_framework'])
 nomoddep = dependency('boost')
 extralibdep = dependency('boost', modules : ['thread', 'system', 'log_setup', 'log'])
 
+pymod = import('python')
+python2 = pymod.find_installation('python2', required: host_machine.system() == 'linux', disabler: true)
+python3 = pymod.find_installation('python3', required: host_machine.system() == 'linux', disabler: true)
+python2dep = python2.dependency(required: host_machine.system() == 'linux', disabler: true)
+python3dep = python3.dependency(required: host_machine.system() == 'linux', disabler: true)
+
+# compile python 2/3 modules only if we found a corresponding python version
+if(python2dep.found() and host_machine.system() == 'linux')
+  if(dep.version().version_compare('>=1.67'))
+    # if we have a new version of boost, we need to construct the module name based
+    # on the installed version of python (and hope that they match the version boost
+    # was compiled against)
+    py2version_string = ''.join(python2dep.version().split('.'))
+    bpython2dep = dependency('boost', modules : ['python' + py2version_string])
+  else
+    # if we have an older version of boost, we need to use the old module names
+    bpython2dep = dependency('boost', modules : ['python'])
+  endif
+
+  if not (bpython2dep.found())
+    bpython2dep = disabler()
+  endif
+else
+  python2dep = disabler()
+  bpython2dep = disabler()
+endif
+
+if(python3dep.found() and host_machine.system() == 'linux')
+  if(dep.version().version_compare('>=1.67'))
+    py3version_string = ''.join(python3dep.version().split('.'))
+    bpython3dep = dependency('boost', modules : ['python' + py3version_string])
+  else
+    bpython3dep = dependency('boost', modules : ['python3'])
+  endif
+
+  if not (bpython3dep.found())
+    bpython3dep = disabler()
+  endif
+else
+  python3dep = disabler()
+  bpython3dep = disabler()
+endif
+
 linkexe = executable('linkedexe', 'linkexe.cc', dependencies : linkdep)
 staticexe = executable('staticlinkedexe', 'linkexe.cc', dependencies : staticdep)
 unitexe = executable('utf', 'unit_test.cpp', dependencies: testdep)
 nomodexe = executable('nomod', 'nomod.cpp', dependencies : nomoddep)
 extralibexe = executable('extralibexe', 'extralib.cpp', dependencies : extralibdep)
 
+# python modules are shared libraries
+python2module = shared_library('python2_module', ['python_module.cpp'], dependencies: [python2dep, bpython2dep], name_prefix: '', cpp_args: ['-DMOD_NAME=python2_module'])
+python3module = shared_library('python3_module', ['python_module.cpp'], dependencies: [python3dep, bpython3dep], name_prefix: '', cpp_args: ['-DMOD_NAME=python3_module'])
+
 test('Boost linktest', linkexe)
 test('Boost statictest', staticexe)
 test('Boost UTF test', unitexe)
 test('Boost nomod', nomodexe)
 test('Boost extralib test', extralibexe)
 
+# explicitly use the correct python interpreter so that we don't have to provide two different python scripts that have different shebang lines
+python2interpreter = find_program(python2.path(), required: false, disabler: true)
+test('Boost Python2', python2interpreter, args: ['./test_python_module.py', meson.current_build_dir()], workdir: meson.current_source_dir(), depends: python2module)
+python3interpreter = find_program(python3.path(), required: false, disabler: true)
+test('Boost Python3', python3interpreter, args: ['./test_python_module.py', meson.current_build_dir()], workdir: meson.current_source_dir(), depends: python2module)
+
 subdir('partial_dep')
 
 # check we can apply a version constraint
diff --git a/test cases/frameworks/1 boost/python_module.cpp b/test cases/frameworks/1 boost/python_module.cpp
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/1 boost/python_module.cpp	
@@ -0,0 +1,22 @@
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <boost/python.hpp>
+
+struct World
+{
+    void set(std::string msg) { this->msg = msg; }
+    std::string greet() { return msg; }
+    std::string version() { return std::to_string(PY_MAJOR_VERSION) + "." + std::to_string(PY_MINOR_VERSION); }
+    std::string msg;
+};
+
+
+BOOST_PYTHON_MODULE(MOD_NAME)
+{
+    using namespace boost::python;
+    class_<World>("World")
+        .def("greet", &World::greet)
+        .def("set", &World::set)
+        .def("version", &World::version)
+    ;
+}
diff --git a/test cases/frameworks/1 boost/test_python_module.py b/test cases/frameworks/1 boost/test_python_module.py
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/1 boost/test_python_module.py	
@@ -0,0 +1,27 @@
+import sys
+sys.path.append(sys.argv[1])
+
+# import compiled python module depending on version of python we are running with
+if sys.version_info[0] == 2:
+    import python2_module
+
+if sys.version_info[0] == 3:
+    import python3_module
+
+
+def run():
+    msg = 'howdy'
+    if sys.version_info[0] == 2:
+        w = python2_module.World()
+
+    if sys.version_info[0] == 3:
+        w = python3_module.World()
+
+    w.set(msg)
+
+    assert(msg == w.greet())
+    version_string = str(sys.version_info[0]) + "." + str(sys.version_info[1])
+    assert(version_string == w.version())
+
+if __name__ == '__main__':
+    run()