1# Copyright 2013-2019 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# This file contains the detection logic for miscellaneous external dependencies.
16
17import functools
18import os
19import re
20import shutil
21import subprocess
22from pathlib import Path
23
24from ..mesonlib import OrderedSet, join_args
25from .base import DependencyException, DependencyMethods
26from .configtool import ConfigToolDependency
27from .pkgconfig import PkgConfigDependency
28from .factory import factory_methods
29import typing as T
30
31if T.TYPE_CHECKING:
32    from .base import Dependency
33    from .factory import DependencyGenerator
34    from ..environment import Environment
35    from ..mesonlib import MachineChoice
36
37
38class HDF5PkgConfigDependency(PkgConfigDependency):
39
40    """Handle brokenness in the HDF5 pkg-config files."""
41
42    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
43        language = language or 'c'
44        if language not in {'c', 'cpp', 'fortran'}:
45            raise DependencyException(f'Language {language} is not supported with HDF5.')
46
47        super().__init__(name, environment, kwargs, language)
48        if not self.is_found:
49            return
50
51        # some broken pkgconfig don't actually list the full path to the needed includes
52        newinc = []  # type: T.List[str]
53        for arg in self.compile_args:
54            if arg.startswith('-I'):
55                stem = 'static' if kwargs.get('static', False) else 'shared'
56                if (Path(arg[2:]) / stem).is_dir():
57                    newinc.append('-I' + str(Path(arg[2:]) / stem))
58        self.compile_args += newinc
59
60        link_args = []  # type: T.List[str]
61        for larg in self.get_link_args():
62            lpath = Path(larg)
63            # some pkg-config hdf5.pc (e.g. Ubuntu) don't include the commonly-used HL HDF5 libraries,
64            # so let's add them if they exist
65            # additionally, some pkgconfig HDF5 HL files are malformed so let's be sure to find HL anyway
66            if lpath.is_file():
67                hl = []
68                if language == 'cpp':
69                    hl += ['_hl_cpp', '_cpp']
70                elif language == 'fortran':
71                    hl += ['_hl_fortran', 'hl_fortran', '_fortran']
72                hl += ['_hl']  # C HL library, always needed
73
74                suffix = '.' + lpath.name.split('.', 1)[1]  # in case of .dll.a
75                for h in hl:
76                    hlfn = lpath.parent / (lpath.name.split('.', 1)[0] + h + suffix)
77                    if hlfn.is_file():
78                        link_args.append(str(hlfn))
79                # HDF5 C libs are required by other HDF5 languages
80                link_args.append(larg)
81            else:
82                link_args.append(larg)
83
84        self.link_args = link_args
85
86
87class HDF5ConfigToolDependency(ConfigToolDependency):
88
89    """Wrapper around hdf5 binary config tools."""
90
91    version_arg = '-showconfig'
92
93    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
94        language = language or 'c'
95        if language not in {'c', 'cpp', 'fortran'}:
96            raise DependencyException(f'Language {language} is not supported with HDF5.')
97
98        if language == 'c':
99            cenv = 'CC'
100            tools = ['h5cc']
101        elif language == 'cpp':
102            cenv = 'CXX'
103            tools = ['h5c++']
104        elif language == 'fortran':
105            cenv = 'FC'
106            tools = ['h5fc']
107        else:
108            raise DependencyException('How did you get here?')
109
110        # We need this before we call super()
111        for_machine = self.get_for_machine_from_kwargs(kwargs)
112
113        nkwargs = kwargs.copy()
114        nkwargs['tools'] = tools
115
116        # Override the compiler that the config tools are going to use by
117        # setting the environment variables that they use for the compiler and
118        # linkers.
119        compiler = environment.coredata.compilers[for_machine][language]
120        try:
121            os.environ[f'HDF5_{cenv}'] = join_args(compiler.get_exelist())
122            os.environ[f'HDF5_{cenv}LINKER'] = join_args(compiler.get_linker_exelist())
123            super().__init__(name, environment, nkwargs, language)
124        finally:
125            del os.environ[f'HDF5_{cenv}']
126            del os.environ[f'HDF5_{cenv}LINKER']
127        if not self.is_found:
128            return
129
130        # We first need to call the tool with -c to get the compile arguments
131        # and then without -c to get the link arguments.
132        args = self.get_config_value(['-show', '-c'], 'args')[1:]
133        args += self.get_config_value(['-show', '-noshlib' if kwargs.get('static', False) else '-shlib'], 'args')[1:]
134        for arg in args:
135            if arg.startswith(('-I', '-f', '-D')) or arg == '-pthread':
136                self.compile_args.append(arg)
137            elif arg.startswith(('-L', '-l', '-Wl')):
138                self.link_args.append(arg)
139            elif Path(arg).is_file():
140                self.link_args.append(arg)
141
142        # If the language is not C we need to add C as a subdependency
143        if language != 'c':
144            nkwargs = kwargs.copy()
145            nkwargs['language'] = 'c'
146            # I'm being too clever for mypy and pylint
147            self.is_found = self._add_sub_dependency(hdf5_factory(environment, for_machine, nkwargs))  # pylint: disable=no-value-for-parameter
148
149    def _sanitize_version(self, ver: str) -> str:
150        v = re.search(r'\s*HDF5 Version: (\d+\.\d+\.\d+)', ver)
151        return v.group(1)
152
153
154@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL})
155def hdf5_factory(env: 'Environment', for_machine: 'MachineChoice',
156                 kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
157    language = kwargs.get('language')
158    candidates: T.List['DependencyGenerator'] = []
159
160    if DependencyMethods.PKGCONFIG in methods:
161        # Use an ordered set so that these remain the first tried pkg-config files
162        pkgconfig_files = OrderedSet(['hdf5', 'hdf5-serial'])
163        # FIXME: This won't honor pkg-config paths, and cross-native files
164        PCEXE = shutil.which('pkg-config')
165        if PCEXE:
166            # some distros put hdf5-1.2.3.pc with version number in .pc filename.
167            ret = subprocess.run([PCEXE, '--list-all'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
168                                    universal_newlines=True)
169            if ret.returncode == 0:
170                for pkg in ret.stdout.split('\n'):
171                    if pkg.startswith('hdf5'):
172                        pkgconfig_files.add(pkg.split(' ', 1)[0])
173
174        for pkg in pkgconfig_files:
175            candidates.append(functools.partial(HDF5PkgConfigDependency, pkg, env, kwargs, language))
176
177    if DependencyMethods.CONFIG_TOOL in methods:
178        candidates.append(functools.partial(HDF5ConfigToolDependency, 'hdf5', env, kwargs, language))
179
180    return candidates
181