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 glob
16import re
17import os
18
19from .. import mlog
20from .. import mesonlib
21from ..environment import detect_cpu_family
22
23from .base import (DependencyException, ExternalDependency)
24
25
26class CudaDependency(ExternalDependency):
27
28    supported_languages = ['cuda', 'cpp', 'c'] # see also _default_language
29
30    def __init__(self, environment, kwargs):
31        compilers = environment.coredata.compilers[self.get_for_machine_from_kwargs(kwargs)]
32        language = self._detect_language(compilers)
33        if language not in self.supported_languages:
34            raise DependencyException('Language \'{}\' is not supported by the CUDA Toolkit. Supported languages are {}.'.format(language, self.supported_languages))
35
36        super().__init__('cuda', environment, kwargs, language=language)
37        self.requested_modules = self.get_requested(kwargs)
38        if 'cudart' not in self.requested_modules:
39            self.requested_modules = ['cudart'] + self.requested_modules
40
41        (self.cuda_path, self.version, self.is_found) = self._detect_cuda_path_and_version()
42        if not self.is_found:
43            return
44
45        if not os.path.isabs(self.cuda_path):
46            raise DependencyException('CUDA Toolkit path must be absolute, got \'{}\'.'.format(self.cuda_path))
47
48        # nvcc already knows where to find the CUDA Toolkit, but if we're compiling
49        # a mixed C/C++/CUDA project, we still need to make the include dir searchable
50        if self.language != 'cuda' or len(compilers) > 1:
51            self.incdir = os.path.join(self.cuda_path, 'include')
52            self.compile_args += ['-I{}'.format(self.incdir)]
53
54        if self.language != 'cuda':
55            arch_libdir = self._detect_arch_libdir()
56            self.libdir = os.path.join(self.cuda_path, arch_libdir)
57            mlog.debug('CUDA library directory is', mlog.bold(self.libdir))
58        else:
59            self.libdir = None
60
61        self.is_found = self._find_requested_libraries()
62
63    @classmethod
64    def _detect_language(cls, compilers):
65        for lang in cls.supported_languages:
66            if lang in compilers:
67                return lang
68        return list(compilers.keys())[0]
69
70    def _detect_cuda_path_and_version(self):
71        self.env_var = self._default_path_env_var()
72        mlog.debug('Default path env var:', mlog.bold(self.env_var))
73
74        version_reqs = self.version_reqs
75        if self.language == 'cuda':
76            nvcc_version = self._strip_patch_version(self.get_compiler().version)
77            mlog.debug('nvcc version:', mlog.bold(nvcc_version))
78            if version_reqs:
79                # make sure nvcc version satisfies specified version requirements
80                (found_some, not_found, found) = mesonlib.version_compare_many(nvcc_version, version_reqs)
81                if not_found:
82                    msg = 'The current nvcc version {} does not satisfy the specified CUDA Toolkit version requirements {}.'.format(nvcc_version, version_reqs)
83                    return self._report_dependency_error(msg, (None, None, False))
84
85            # use nvcc version to find a matching CUDA Toolkit
86            version_reqs = ['={}'.format(nvcc_version)]
87        else:
88            nvcc_version = None
89
90        paths = [(path, self._cuda_toolkit_version(path), default) for (path, default) in self._cuda_paths()]
91        if version_reqs:
92            return self._find_matching_toolkit(paths, version_reqs, nvcc_version)
93
94        defaults = [(path, version) for (path, version, default) in paths if default]
95        if defaults:
96            return (defaults[0][0], defaults[0][1], True)
97
98        platform_msg = 'set the CUDA_PATH environment variable' if self._is_windows() \
99            else 'set the CUDA_PATH environment variable/create the \'/usr/local/cuda\' symbolic link'
100        msg = 'Please specify the desired CUDA Toolkit version (e.g. dependency(\'cuda\', version : \'>=10.1\')) or {} to point to the location of your desired version.'.format(platform_msg)
101        return self._report_dependency_error(msg, (None, None, False))
102
103    def _find_matching_toolkit(self, paths, version_reqs, nvcc_version):
104        # keep the default paths order intact, sort the rest in the descending order
105        # according to the toolkit version
106        defaults, rest = mesonlib.partition(lambda t: not t[2], paths)
107        defaults = list(defaults)
108        paths = defaults + sorted(rest, key=lambda t: mesonlib.Version(t[1]), reverse=True)
109        mlog.debug('Search paths: {}'.format(paths))
110
111        if nvcc_version and defaults:
112            default_src = "the {} environment variable".format(self.env_var) if self.env_var else "the \'/usr/local/cuda\' symbolic link"
113            nvcc_warning = 'The default CUDA Toolkit as designated by {} ({}) doesn\'t match the current nvcc version {} and will be ignored.'.format(default_src, os.path.realpath(defaults[0][0]), nvcc_version)
114        else:
115            nvcc_warning = None
116
117        for (path, version, default) in paths:
118            (found_some, not_found, found) = mesonlib.version_compare_many(version, version_reqs)
119            if not not_found:
120                if not default and nvcc_warning:
121                    mlog.warning(nvcc_warning)
122                return (path, version, True)
123
124        if nvcc_warning:
125            mlog.warning(nvcc_warning)
126        return (None, None, False)
127
128    def _default_path_env_var(self):
129        env_vars = ['CUDA_PATH'] if self._is_windows() else ['CUDA_PATH', 'CUDA_HOME', 'CUDA_ROOT']
130        env_vars = [var for var in env_vars if var in os.environ]
131        user_defaults = set([os.environ[var] for var in env_vars])
132        if len(user_defaults) > 1:
133            mlog.warning('Environment variables {} point to conflicting toolkit locations ({}). Toolkit selection might produce unexpected results.'.format(', '.join(env_vars), ', '.join(user_defaults)))
134        return env_vars[0] if env_vars else None
135
136    def _cuda_paths(self):
137        return ([(os.environ[self.env_var], True)] if self.env_var else []) \
138            + (self._cuda_paths_win() if self._is_windows() else self._cuda_paths_nix())
139
140    def _cuda_paths_win(self):
141        env_vars = os.environ.keys()
142        return [(os.environ[var], False) for var in env_vars if var.startswith('CUDA_PATH_')]
143
144    def _cuda_paths_nix(self):
145        # include /usr/local/cuda default only if no env_var was found
146        pattern = '/usr/local/cuda-*' if self.env_var else '/usr/local/cuda*'
147        return [(path, os.path.basename(path) == 'cuda') for path in glob.iglob(pattern)]
148
149    toolkit_version_regex = re.compile(r'^CUDA Version\s+(.*)$')
150    path_version_win_regex = re.compile(r'^v(.*)$')
151    path_version_nix_regex = re.compile(r'^cuda-(.*)$')
152
153    def _cuda_toolkit_version(self, path):
154        version = self._read_toolkit_version_txt(path)
155        if version:
156            return version
157
158        mlog.debug('Falling back to extracting version from path')
159        path_version_regex = self.path_version_win_regex if self._is_windows() else self.path_version_nix_regex
160        try:
161            m = path_version_regex.match(os.path.basename(path))
162            if m:
163                return m.group(1)
164            else:
165                mlog.warning('Could not detect CUDA Toolkit version for {}'.format(path))
166        except Exception as e:
167            mlog.warning('Could not detect CUDA Toolkit version for {}: {}'.format(path, str(e)))
168
169        return '0.0'
170
171    def _read_toolkit_version_txt(self, path):
172        # Read 'version.txt' at the root of the CUDA Toolkit directory to determine the tookit version
173        version_file_path = os.path.join(path, 'version.txt')
174        try:
175            with open(version_file_path) as version_file:
176                version_str = version_file.readline() # e.g. 'CUDA Version 10.1.168'
177                m = self.toolkit_version_regex.match(version_str)
178                if m:
179                    return self._strip_patch_version(m.group(1))
180        except Exception as e:
181            mlog.debug('Could not read CUDA Toolkit\'s version file {}: {}'.format(version_file_path, str(e)))
182
183        return None
184
185    @classmethod
186    def _strip_patch_version(cls, version):
187        return '.'.join(version.split('.')[:2])
188
189    def _detect_arch_libdir(self):
190        arch = detect_cpu_family(self.env.coredata.compilers.host)
191        machine = self.env.machines[self.for_machine]
192        msg = '{} architecture is not supported in {} version of the CUDA Toolkit.'
193        if machine.is_windows():
194            libdirs = {'x86': 'Win32', 'x86_64': 'x64'}
195            if arch not in libdirs:
196                raise DependencyException(msg.format(arch, 'Windows'))
197            return os.path.join('lib', libdirs[arch])
198        elif machine.is_linux():
199            libdirs = {'x86_64': 'lib64', 'ppc64': 'lib', 'aarch64': 'lib64'}
200            if arch not in libdirs:
201                raise DependencyException(msg.format(arch, 'Linux'))
202            return libdirs[arch]
203        elif machine.is_osx():
204            libdirs = {'x86_64': 'lib64'}
205            if arch not in libdirs:
206                raise DependencyException(msg.format(arch, 'macOS'))
207            return libdirs[arch]
208        else:
209            raise DependencyException('CUDA Toolkit: unsupported platform.')
210
211    def _find_requested_libraries(self):
212        self.lib_modules = {}
213        all_found = True
214
215        for module in self.requested_modules:
216            args = self.clib_compiler.find_library(module, self.env, [self.libdir] if self.libdir else [])
217            if args is None:
218                self._report_dependency_error('Couldn\'t find requested CUDA module \'{}\''.format(module))
219                all_found = False
220            else:
221                mlog.debug('Link args for CUDA module \'{}\' are {}'.format(module, args))
222                self.lib_modules[module] = args
223
224        return all_found
225
226    def _is_windows(self):
227        return self.env.machines[self.for_machine].is_windows()
228
229    def _report_dependency_error(self, msg, ret_val=None):
230        if self.required:
231            raise DependencyException(msg)
232
233        mlog.debug(msg)
234        return ret_val
235
236    def log_details(self):
237        module_str = ', '.join(self.requested_modules)
238        return 'modules: ' + module_str
239
240    def log_info(self):
241        return self.cuda_path if self.cuda_path else ''
242
243    def get_requested(self, kwargs):
244        candidates = mesonlib.extract_as_list(kwargs, 'modules')
245        for c in candidates:
246            if not isinstance(c, str):
247                raise DependencyException('CUDA module argument is not a string.')
248        return candidates
249
250    def get_link_args(self, **kwargs):
251        args = []
252        if self.libdir:
253            args += self.clib_compiler.get_linker_search_args(self.libdir)
254        for lib in self.requested_modules:
255            args += self.lib_modules[lib]
256        return args
257