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
15import functools
16import typing as T
17import os
18import re
19
20from ..environment import detect_cpu_family
21from .base import DependencyMethods, detect_compiler, SystemDependency
22from .configtool import ConfigToolDependency
23from .factory import factory_methods
24from .pkgconfig import PkgConfigDependency
25
26if T.TYPE_CHECKING:
27    from .factory import DependencyGenerator
28    from ..environment import Environment, MachineChoice
29
30
31@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.SYSTEM})
32def mpi_factory(env: 'Environment',
33                for_machine: 'MachineChoice',
34                kwargs: T.Dict[str, T.Any],
35                methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
36    language = kwargs.get('language', 'c')
37    if language not in {'c', 'cpp', 'fortran'}:
38        # OpenMPI doesn't work without any other languages
39        return []
40
41    candidates: T.List['DependencyGenerator'] = []
42    compiler = detect_compiler('mpi', env, for_machine, language)
43    if compiler is None:
44        return []
45    compiler_is_intel = compiler.get_id() in {'intel', 'intel-cl'}
46
47    # Only OpenMPI has pkg-config, and it doesn't work with the intel compilers
48    if DependencyMethods.PKGCONFIG in methods and not compiler_is_intel:
49        pkg_name = None
50        if language == 'c':
51            pkg_name = 'ompi-c'
52        elif language == 'cpp':
53            pkg_name = 'ompi-cxx'
54        elif language == 'fortran':
55            pkg_name = 'ompi-fort'
56        candidates.append(functools.partial(
57            PkgConfigDependency, pkg_name, env, kwargs, language=language))
58
59    if DependencyMethods.CONFIG_TOOL in methods:
60        nwargs = kwargs.copy()
61
62        if compiler_is_intel:
63            if env.machines[for_machine].is_windows():
64                nwargs['version_arg'] = '-v'
65                nwargs['returncode_value'] = 3
66
67            if language == 'c':
68                tool_names = [os.environ.get('I_MPI_CC'), 'mpiicc']
69            elif language == 'cpp':
70                tool_names = [os.environ.get('I_MPI_CXX'), 'mpiicpc']
71            elif language == 'fortran':
72                tool_names = [os.environ.get('I_MPI_F90'), 'mpiifort']
73
74            cls = IntelMPIConfigToolDependency  # type: T.Type[ConfigToolDependency]
75        else: # OpenMPI, which doesn't work with intel
76            #
77            # We try the environment variables for the tools first, but then
78            # fall back to the hardcoded names
79            if language == 'c':
80                tool_names = [os.environ.get('MPICC'), 'mpicc']
81            elif language == 'cpp':
82                tool_names = [os.environ.get('MPICXX'), 'mpic++', 'mpicxx', 'mpiCC']
83            elif language == 'fortran':
84                tool_names = [os.environ.get(e) for e in ['MPIFC', 'MPIF90', 'MPIF77']]
85                tool_names.extend(['mpifort', 'mpif90', 'mpif77'])
86
87            cls = OpenMPIConfigToolDependency
88
89        tool_names = [t for t in tool_names if t]  # remove empty environment variables
90        assert tool_names
91
92        nwargs['tools'] = tool_names
93        candidates.append(functools.partial(
94            cls, tool_names[0], env, nwargs, language=language))
95
96    if DependencyMethods.SYSTEM in methods:
97        candidates.append(functools.partial(
98            MSMPIDependency, 'msmpi', env, kwargs, language=language))
99
100    return candidates
101
102
103class _MPIConfigToolDependency(ConfigToolDependency):
104
105    def _filter_compile_args(self, args: T.Sequence[str]) -> T.List[str]:
106        """
107        MPI wrappers return a bunch of garbage args.
108        Drop -O2 and everything that is not needed.
109        """
110        result = []
111        multi_args: T.Tuple[str, ...] = ('-I', )
112        if self.language == 'fortran':
113            fc = self.env.coredata.compilers[self.for_machine]['fortran']
114            multi_args += fc.get_module_incdir_args()
115
116        include_next = False
117        for f in args:
118            if f.startswith(('-D', '-f') + multi_args) or f == '-pthread' \
119                    or (f.startswith('-W') and f != '-Wall' and not f.startswith('-Werror')):
120                result.append(f)
121                if f in multi_args:
122                    # Path is a separate argument.
123                    include_next = True
124            elif include_next:
125                include_next = False
126                result.append(f)
127        return result
128
129    def _filter_link_args(self, args: T.Sequence[str]) -> T.List[str]:
130        """
131        MPI wrappers return a bunch of garbage args.
132        Drop -O2 and everything that is not needed.
133        """
134        result = []
135        include_next = False
136        for f in args:
137            if self._is_link_arg(f):
138                result.append(f)
139                if f in ('-L', '-Xlinker'):
140                    include_next = True
141            elif include_next:
142                include_next = False
143                result.append(f)
144        return result
145
146    def _is_link_arg(self, f: str) -> bool:
147        if self.clib_compiler.id == 'intel-cl':
148            return f == '/link' or f.startswith('/LIBPATH') or f.endswith('.lib')   # always .lib whether static or dynamic
149        else:
150            return (f.startswith(('-L', '-l', '-Xlinker')) or
151                    f == '-pthread' or
152                    (f.startswith('-W') and f != '-Wall' and not f.startswith('-Werror')))
153
154
155class IntelMPIConfigToolDependency(_MPIConfigToolDependency):
156
157    """Wrapper around Intel's mpiicc and friends."""
158
159    version_arg = '-v'  # --version is not the same as -v
160
161    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
162                 language: T.Optional[str] = None):
163        super().__init__(name, env, kwargs, language=language)
164        if not self.is_found:
165            return
166
167        args = self.get_config_value(['-show'], 'link and compile args')
168        self.compile_args = self._filter_compile_args(args)
169        self.link_args = self._filter_link_args(args)
170
171    def _sanitize_version(self, out: str) -> str:
172        v = re.search(r'(\d{4}) Update (\d)', out)
173        if v:
174            return '{}.{}'.format(v.group(1), v.group(2))
175        return out
176
177
178class OpenMPIConfigToolDependency(_MPIConfigToolDependency):
179
180    """Wrapper around OpenMPI mpicc and friends."""
181
182    version_arg = '--showme:version'
183
184    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
185                 language: T.Optional[str] = None):
186        super().__init__(name, env, kwargs, language=language)
187        if not self.is_found:
188            return
189
190        c_args = self.get_config_value(['--showme:compile'], 'compile_args')
191        self.compile_args = self._filter_compile_args(c_args)
192
193        l_args = self.get_config_value(['--showme:link'], 'link_args')
194        self.link_args = self._filter_link_args(l_args)
195
196    def _sanitize_version(self, out: str) -> str:
197        v = re.search(r'\d+.\d+.\d+', out)
198        if v:
199            return v.group(0)
200        return out
201
202
203class MSMPIDependency(SystemDependency):
204
205    """The Microsoft MPI."""
206
207    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
208                 language: T.Optional[str] = None):
209        super().__init__(name, env, kwargs, language=language)
210        # MSMPI only supports the C API
211        if language not in {'c', 'fortran', None}:
212            self.is_found = False
213            return
214        # MSMPI is only for windows, obviously
215        if not self.env.machines[self.for_machine].is_windows():
216            return
217
218        incdir = os.environ.get('MSMPI_INC')
219        arch = detect_cpu_family(self.env.coredata.compilers.host)
220        libdir = None
221        if arch == 'x86':
222            libdir = os.environ.get('MSMPI_LIB32')
223            post = 'x86'
224        elif arch == 'x86_64':
225            libdir = os.environ.get('MSMPI_LIB64')
226            post = 'x64'
227
228        if libdir is None or incdir is None:
229            self.is_found = False
230            return
231
232        self.is_found = True
233        self.link_args = ['-l' + os.path.join(libdir, 'msmpi')]
234        self.compile_args = ['-I' + incdir, '-I' + os.path.join(incdir, post)]
235        if self.language == 'fortran':
236            self.link_args.append('-l' + os.path.join(libdir, 'msmpifec'))
237