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