1# Copyright 2013-2021 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
15from .base import ExternalDependency, DependencyException, DependencyTypeName
16from .pkgconfig import PkgConfigDependency
17from ..mesonlib import Popen_safe
18from ..programs import ExternalProgram
19from ..compilers import DCompiler
20from .. import mlog
21import re
22import os
23import copy
24import json
25import platform
26import typing as T
27
28if T.TYPE_CHECKING:
29    from ..environment import Environment
30
31class DubDependency(ExternalDependency):
32    class_dubbin = None
33
34    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
35        super().__init__(DependencyTypeName('dub'), environment, kwargs, language='d')
36        self.name = name
37        self.module_path: T.Optional[str] = None
38
39        _temp_comp = super().get_compiler()
40        assert isinstance(_temp_comp, DCompiler)
41        self.compiler = _temp_comp
42
43        if 'required' in kwargs:
44            self.required = kwargs.get('required')
45
46        if DubDependency.class_dubbin is None:
47            self.dubbin = self._check_dub()
48            DubDependency.class_dubbin = self.dubbin
49        else:
50            self.dubbin = DubDependency.class_dubbin
51
52        if not self.dubbin:
53            if self.required:
54                raise DependencyException('DUB not found.')
55            self.is_found = False
56            return
57
58        assert isinstance(self.dubbin, ExternalProgram)
59        mlog.debug('Determining dependency {!r} with DUB executable '
60                   '{!r}'.format(name, self.dubbin.get_path()))
61
62        # we need to know the target architecture
63        arch = self.compiler.arch
64
65        # Ask dub for the package
66        ret, res = self._call_dubbin(['describe', name, '--arch=' + arch])
67
68        if ret != 0:
69            self.is_found = False
70            return
71
72        comp = self.compiler.get_id().replace('llvm', 'ldc').replace('gcc', 'gdc')
73        packages = []
74        description = json.loads(res)
75        for package in description['packages']:
76            packages.append(package['name'])
77            if package['name'] == name:
78                self.is_found = True
79
80                not_lib = True
81                if 'targetType' in package:
82                    if package['targetType'] in ['library', 'sourceLibrary', 'staticLibrary', 'dynamicLibrary']:
83                        not_lib = False
84
85                if not_lib:
86                    mlog.error(mlog.bold(name), "found but it isn't a library")
87                    self.is_found = False
88                    return
89
90                self.module_path = self._find_right_lib_path(package['path'], comp, description, True, package['targetFileName'])
91                if not os.path.exists(self.module_path):
92                    # check if the dependency was built for other archs
93                    archs = [['x86_64'], ['x86'], ['x86', 'x86_mscoff']]
94                    for a in archs:
95                        description_a = copy.deepcopy(description)
96                        description_a['architecture'] = a
97                        arch_module_path = self._find_right_lib_path(package['path'], comp, description_a, True, package['targetFileName'])
98                        if arch_module_path:
99                            mlog.error(mlog.bold(name), "found but it wasn't compiled for", mlog.bold(arch))
100                            self.is_found = False
101                            return
102
103                    mlog.error(mlog.bold(name), "found but it wasn't compiled with", mlog.bold(comp))
104                    self.is_found = False
105                    return
106
107                self.version = package['version']
108                self.pkg = package
109
110        if self.pkg['targetFileName'].endswith('.a'):
111            self.static = True
112
113        self.compile_args = []
114        for flag in self.pkg['dflags']:
115            self.link_args.append(flag)
116        for path in self.pkg['importPaths']:
117            self.compile_args.append('-I' + os.path.join(self.pkg['path'], path))
118
119        self.link_args = self.raw_link_args = []
120        for flag in self.pkg['lflags']:
121            self.link_args.append(flag)
122
123        self.link_args.append(os.path.join(self.module_path, self.pkg['targetFileName']))
124
125        # Handle dependencies
126        libs = []
127
128        def add_lib_args(field_name: str, target: T.Dict[str, T.Dict[str, str]]) -> None:
129            if field_name in target['buildSettings']:
130                for lib in target['buildSettings'][field_name]:
131                    if lib not in libs:
132                        libs.append(lib)
133                        if os.name != 'nt':
134                            pkgdep = PkgConfigDependency(lib, environment, {'required': 'true', 'silent': 'true'})
135                            for arg in pkgdep.get_compile_args():
136                                self.compile_args.append(arg)
137                            for arg in pkgdep.get_link_args():
138                                self.link_args.append(arg)
139                            for arg in pkgdep.get_link_args(raw=True):
140                                self.raw_link_args.append(arg)
141
142        for target in description['targets']:
143            if target['rootPackage'] in packages:
144                add_lib_args('libs', target)
145                add_lib_args(f'libs-{platform.machine()}', target)
146                for file in target['buildSettings']['linkerFiles']:
147                    lib_path = self._find_right_lib_path(file, comp, description)
148                    if lib_path:
149                        self.link_args.append(lib_path)
150                    else:
151                        self.is_found = False
152
153    def _find_right_lib_path(self,
154                             default_path: str,
155                             comp: str,
156                             description: T.Dict[str, str],
157                             folder_only: bool = False,
158                             file_name: str = '') -> T.Optional[str]:
159        module_path = lib_file_name = ''
160        if folder_only:
161            module_path = default_path
162            lib_file_name = file_name
163        else:
164            module_path = os.path.dirname(default_path)
165            lib_file_name = os.path.basename(default_path)
166        module_build_path = os.path.join(module_path, '.dub', 'build')
167
168        # If default_path is a path to lib file and
169        # directory of lib don't have subdir '.dub/build'
170        if not os.path.isdir(module_build_path) and os.path.isfile(default_path):
171            if folder_only:
172                return module_path
173            else:
174                return default_path
175
176        # Get D version implemented in the compiler
177        # gdc doesn't support this
178        ret, res = self._call_dubbin(['--version'])
179
180        if ret != 0:
181            mlog.error('Failed to run {!r}', mlog.bold(comp))
182            return None
183
184        d_ver_reg = re.search('v[0-9].[0-9][0-9][0-9].[0-9]', res) # Ex.: v2.081.2
185        if d_ver_reg is not None:
186            d_ver = d_ver_reg.group().rsplit('.', 1)[0].replace('v', '').replace('.', '') # Fix structure. Ex.: 2081
187        else:
188            d_ver = '' # gdc
189
190        if not os.path.isdir(module_build_path):
191            return ''
192
193        # Ex.: library-debug-linux.posix-x86_64-ldc_2081-EF934983A3319F8F8FF2F0E107A363BA
194        build_name = '-{}-{}-{}-{}_{}'.format(description['buildType'], '.'.join(description['platform']), '.'.join(description['architecture']), comp, d_ver)
195        for entry in os.listdir(module_build_path):
196            if build_name in entry:
197                for file in os.listdir(os.path.join(module_build_path, entry)):
198                    if file == lib_file_name:
199                        if folder_only:
200                            return os.path.join(module_build_path, entry)
201                        else:
202                            return os.path.join(module_build_path, entry, lib_file_name)
203
204        return ''
205
206    def _call_dubbin(self, args: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str]:
207        assert isinstance(self.dubbin, ExternalProgram)
208        p, out = Popen_safe(self.dubbin.get_command() + args, env=env)[0:2]
209        return p.returncode, out.strip()
210
211    def _call_copmbin(self, args: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str]:
212        p, out = Popen_safe(self.compiler.get_exelist() + args, env=env)[0:2]
213        return p.returncode, out.strip()
214
215    def _check_dub(self) -> T.Union[bool, ExternalProgram]:
216        dubbin: T.Union[bool, ExternalProgram] = ExternalProgram('dub', silent=True)
217        assert isinstance(dubbin, ExternalProgram)
218        if dubbin.found():
219            try:
220                p, out = Popen_safe(dubbin.get_command() + ['--version'])[0:2]
221                if p.returncode != 0:
222                    mlog.warning('Found dub {!r} but couldn\'t run it'
223                                 ''.format(' '.join(dubbin.get_command())))
224                    # Set to False instead of None to signify that we've already
225                    # searched for it and not found it
226                    dubbin = False
227            except (FileNotFoundError, PermissionError):
228                dubbin = False
229        else:
230            dubbin = False
231        if isinstance(dubbin, ExternalProgram):
232            mlog.log('Found DUB:', mlog.bold(dubbin.get_path()),
233                     '(%s)' % out.strip())
234        else:
235            mlog.log('Found DUB:', mlog.red('NO'))
236        return dubbin
237