Repository: mesonbuild/meson

Issue:
regression: boost dependency with the python module not found
**Describe the bug**

Regression in detecting the python boost module 

**To Reproduce**

Consider the following `meson.build` (extracted from tizonia, listed in Users.md)
```
project('boost', 'cpp')
boost_dep = dependency(
  'boost', modules: ['chrono', 'filesystem', 'system', 'thread', 'program_options', 'python3'],
  required: true, version: '>=1.54'
)
```

On Ubuntu 18.04, with the  `libboost-chrono-dev`, `libboost-filesystem-dev`, `libboost-program-options-dev`, `libboost-python-dev`, `libboost-thread-dev` packages all installed.  `cmake` is not installed.

with meson 0.54.0.rc1
```
The Meson build system
Version: 0.53.999
Source dir: /home/jon/src/meson/test cases/frameworks/99 more
Build dir: /home/jon/src/meson/test cases/frameworks/99 more/_build
Build type: native build
Project name: boost
Project version: undefined
C++ compiler for the host machine: c++ (gcc 7.5.0 "c++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0")
C++ linker for the host machine: c++ GNU ld.bfd 2.30
Host machine cpu family: x86_64
Host machine cpu: x86_64
Run-time dependency Boost (modules: chrono, filesystem, program_options, python3, system, thread) found: NO 

meson.build:2:0: ERROR: Dependency "boost" not found
```

with meson 0.53.0
```
The Meson build system
Version: 0.53.0
Source dir: /home/jon/src/meson/test cases/frameworks/99 more
Build dir: /home/jon/src/meson/test cases/frameworks/99 more/_build
Build type: native build
Project name: boost
Project version: undefined
C++ compiler for the host machine: c++ (gcc 7.5.0 "c++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0")
C++ linker for the host machine: c++ GNU ld.bfd 2.30
Host machine cpu family: x86_64
Host machine cpu: x86_64
Run-time dependency Boost (chrono, filesystem, system, thread, program_options, python3) found: YES 1.65.1
Build targets in project: 0

Found ninja-1.8.2 at /usr/bin/ninja
```


bisection lands on commit 08224dafcba1b694fb624553e7d84deb565aae22

**Expected behavior**

* the python boost module should be found
* when a specific boost module cannot be found, that should be identifed in or close to the error, not just written to the log.

**system parameters**
* native build
* Ubuntu 18.04
* Python 3.6.9
* 0.53.0, and 0.54.0.rc1



Discussion:
CC @mensinda 

There appears to be some coverage of this in `test cases\frameworks\1 boost`, but I guess we aren't hitting that in CI because we always have cmake installed?

Neither CMake nor pkg-config are used for finding boost. Also, boost python is really anoying to find because you have to provide the major *and* the minor version. See the [test meson.build](https://github.com/mesonbuild/meson/blob/master/test%20cases/frameworks/1%20boost/meson.build#L33).

So, just passing `python3` as a module, shouldn't have worked even before my boost rewrite.

Also, I will look into better logging which module wasn't found.

Resolving patch:
diff --git a/mesonbuild/dependencies/boost.py b/mesonbuild/dependencies/boost.py
--- a/mesonbuild/dependencies/boost.py
+++ b/mesonbuild/dependencies/boost.py
@@ -101,6 +101,11 @@ def __lt__(self, other: T.Any) -> bool:
 
 @functools.total_ordering
 class BoostLibraryFile():
+    # Python libraries are special because of the included
+    # minor version in the module name.
+    boost_python_libs = ['boost_python', 'boost_numpy']
+    reg_python_mod_split = re.compile(r'(boost_[a-zA-Z]+)([0-9]*)')
+
     reg_abi_tag = re.compile(r'^s?g?y?d?p?n?$')
     reg_ver_tag = re.compile(r'^[0-9_]+$')
 
@@ -218,6 +223,33 @@ def abitag(self) -> str:
     def is_boost(self) -> bool:
         return any([self.name.startswith(x) for x in ['libboost_', 'boost_']])
 
+    def is_python_lib(self) -> bool:
+        return any([self.mod_name.startswith(x) for x in BoostLibraryFile.boost_python_libs])
+
+    def mod_name_matches(self, mod_name: str) -> bool:
+        if self.mod_name == mod_name:
+            return True
+        if not self.is_python_lib():
+            return False
+
+        m_cur = BoostLibraryFile.reg_python_mod_split.match(self.mod_name)
+        m_arg = BoostLibraryFile.reg_python_mod_split.match(mod_name)
+
+        if not m_cur or not m_arg:
+            return False
+
+        if m_cur.group(1) != m_arg.group(1):
+            return False
+
+        cur_vers = m_cur.group(2)
+        arg_vers = m_arg.group(2)
+
+        # Always assume python 2 if nothing is specified
+        if not arg_vers:
+            arg_vers = '2'
+
+        return cur_vers.startswith(arg_vers)
+
     def version_matches(self, version_lib: str) -> bool:
         # If no version tag is present, assume that it fits
         if not self.version_lib or not version_lib:
@@ -279,6 +311,9 @@ def __init__(self, environment: Environment, kwargs):
             if i.startswith('boost_'):
                 raise DependencyException('Boost modules must be passed without the boost_ prefix')
 
+        self.modules_found = []    # type: T.List[str]
+        self.modules_missing = []  # type: T.List[str]
+
         # Do we need threads?
         if 'thread' in self.modules:
             if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
@@ -358,7 +393,7 @@ def run_check(self, inc_dirs: T.List[BoostIncludeDir], lib_dirs: T.List[Path]) -
             for mod in modules:
                 found = False
                 for l in f_libs:
-                    if l.mod_name == mod:
+                    if l.mod_name_matches(mod):
                         selected_modules += [l]
                         found = True
                         break
@@ -379,6 +414,13 @@ def run_check(self, inc_dirs: T.List[BoostIncludeDir], lib_dirs: T.List[Path]) -
             comp_args = list(set(comp_args))
             link_args = list(set(link_args))
 
+            self.modules_found = [x.mod_name for x in selected_modules]
+            self.modules_found = [x[6:] for x in self.modules_found]
+            self.modules_found = sorted(set(self.modules_found))
+            self.modules_missing = not_found
+            self.modules_missing = [x[6:] for x in self.modules_missing]
+            self.modules_missing = sorted(set(self.modules_missing))
+
             # if we found all modules we are done
             if not not_found:
                 self.version = inc.version
@@ -526,10 +568,14 @@ def detect_roots(self) -> T.List[Path]:
         return roots
 
     def log_details(self) -> str:
-        modules = sorted(set(self.modules))
-        if modules:
-            return 'modules: ' + ', '.join(modules)
-        return ''
+        res = ''
+        if self.modules_found:
+            res += 'found: ' + ', '.join(self.modules_found)
+        if self.modules_missing:
+            if res:
+                res += ' | '
+            res += 'missing: ' + ', '.join(self.modules_missing)
+        return res
 
     def log_info(self) -> str:
         if self.boost_root:


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	
@@ -29,28 +29,14 @@ python3dep = python3.dependency(required: host_machine.system() == 'linux', embe
 
 # compile python 2/3 modules only if we found a corresponding python version
 if(python2dep.found() and host_machine.system() == 'linux' and not s)
-  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', static: s, modules : ['python' + py2version_string], required: false, disabler: true)
-  else
-    # if we have an older version of boost, we need to use the old module names
-    bpython2dep = dependency('boost', static: s, modules : ['python'], required: false, disabler: true)
-  endif
+  bpython2dep = dependency('boost', static: s, modules : ['python'], required: false, disabler: true)
 else
   python2dep = disabler()
   bpython2dep = disabler()
 endif
 
 if(python3dep.found() and host_machine.system() == 'linux' and not s)
-  if(dep.version().version_compare('>=1.67'))
-    py3version_string = ''.join(python3dep.version().split('.'))
-    bpython3dep = dependency('boost', static: s, modules : ['python' + py3version_string], required: false, disabler: true)
-  else
-    bpython3dep = dependency('boost', static: s, modules : ['python3'], required: false, disabler: true)
-  endif
+  bpython3dep = dependency('boost', static: s, modules : ['python3'], required: false, disabler: true)
 else
   python3dep = disabler()
   bpython3dep = disabler()