# Copyright 2013-2019 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import glob import re import os from .. import mlog from .. import mesonlib from ..environment import detect_cpu_family from .base import (DependencyException, ExternalDependency) class CudaDependency(ExternalDependency): supported_languages = ['cuda', 'cpp', 'c'] # see also _default_language def __init__(self, environment, kwargs): compilers = environment.coredata.compilers[self.get_for_machine_from_kwargs(kwargs)] language = self._detect_language(compilers) if language not in self.supported_languages: raise DependencyException('Language \'{}\' is not supported by the CUDA Toolkit. Supported languages are {}.'.format(language, self.supported_languages)) super().__init__('cuda', environment, language, kwargs) self.requested_modules = self.get_requested(kwargs) if 'cudart' not in self.requested_modules: self.requested_modules = ['cudart'] + self.requested_modules (self.cuda_path, self.version, self.is_found) = self._detect_cuda_path_and_version() if not self.is_found: return if not os.path.isabs(self.cuda_path): raise DependencyException('CUDA Toolkit path must be absolute, got \'{}\'.'.format(self.cuda_path)) # nvcc already knows where to find the CUDA Toolkit, but if we're compiling # a mixed C/C++/CUDA project, we still need to make the include dir searchable if self.language != 'cuda' or len(compilers) > 1: self.incdir = os.path.join(self.cuda_path, 'include') self.compile_args += ['-I{}'.format(self.incdir)] if self.language != 'cuda': arch_libdir = self._detect_arch_libdir() self.libdir = os.path.join(self.cuda_path, arch_libdir) mlog.debug('CUDA library directory is', mlog.bold(self.libdir)) else: self.libdir = None self.is_found = self._find_requested_libraries() @classmethod def _detect_language(cls, compilers): for lang in cls.supported_languages: if lang in compilers: return lang return list(compilers.keys())[0] def _detect_cuda_path_and_version(self): self.env_var = self._default_path_env_var() mlog.debug('Default path env var:', mlog.bold(self.env_var)) version_reqs = self.version_reqs if self.language == 'cuda': nvcc_version = self._strip_patch_version(self.get_compiler().version) mlog.debug('nvcc version:', mlog.bold(nvcc_version)) if version_reqs: # make sure nvcc version satisfies specified version requirements (found_some, not_found, found) = mesonlib.version_compare_many(nvcc_version, version_reqs) if not_found: msg = 'The current nvcc version {} does not satisfy the specified CUDA Toolkit version requirements {}.'.format(nvcc_version, version_reqs) return self._report_dependency_error(msg, (None, None, False)) # use nvcc version to find a matching CUDA Toolkit version_reqs = ['={}'.format(nvcc_version)] else: nvcc_version = None paths = [(path, self._cuda_toolkit_version(path), default) for (path, default) in self._cuda_paths()] if version_reqs: return self._find_matching_toolkit(paths, version_reqs, nvcc_version) defaults = [(path, version) for (path, version, default) in paths if default] if defaults: return (defaults[0][0], defaults[0][1], True) platform_msg = 'set the CUDA_PATH environment variable' if self._is_windows() \ else 'set the CUDA_PATH environment variable/create the \'/usr/local/cuda\' symbolic link' 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) return self._report_dependency_error(msg, (None, None, False)) def _find_matching_toolkit(self, paths, version_reqs, nvcc_version): # keep the default paths order intact, sort the rest in the descending order # according to the toolkit version defaults, rest = mesonlib.partition(lambda t: not t[2], paths) defaults = list(defaults) paths = defaults + sorted(rest, key=lambda t: mesonlib.Version(t[1]), reverse=True) mlog.debug('Search paths: {}'.format(paths)) if nvcc_version and defaults: default_src = "the {} environment variable".format(self.env_var) if self.env_var else "the \'/usr/local/cuda\' symbolic link" 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) else: nvcc_warning = None for (path, version, default) in paths: (found_some, not_found, found) = mesonlib.version_compare_many(version, version_reqs) if not not_found: if not default and nvcc_warning: mlog.warning(nvcc_warning) return (path, version, True) if nvcc_warning: mlog.warning(nvcc_warning) return (None, None, False) def _default_path_env_var(self): env_vars = ['CUDA_PATH'] if self._is_windows() else ['CUDA_PATH', 'CUDA_HOME', 'CUDA_ROOT'] env_vars = [var for var in env_vars if var in os.environ] user_defaults = set([os.environ[var] for var in env_vars]) if len(user_defaults) > 1: mlog.warning('Environment variables {} point to conflicting toolkit locations ({}). Toolkit selection might produce unexpected results.'.format(', '.join(env_vars), ', '.join(user_defaults))) return env_vars[0] if env_vars else None def _cuda_paths(self): return ([(os.environ[self.env_var], True)] if self.env_var else []) \ + (self._cuda_paths_win() if self._is_windows() else self._cuda_paths_nix()) def _cuda_paths_win(self): env_vars = os.environ.keys() return [(os.environ[var], False) for var in env_vars if var.startswith('CUDA_PATH_')] def _cuda_paths_nix(self): # include /usr/local/cuda default only if no env_var was found pattern = '/usr/local/cuda-*' if self.env_var else '/usr/local/cuda*' return [(path, os.path.basename(path) == 'cuda') for path in glob.iglob(pattern)] toolkit_version_regex = re.compile(r'^CUDA Version\s+(.*)$') path_version_win_regex = re.compile(r'^v(.*)$') path_version_nix_regex = re.compile(r'^cuda-(.*)$') def _cuda_toolkit_version(self, path): version = self._read_toolkit_version_txt(path) if version: return version mlog.debug('Falling back to extracting version from path') path_version_regex = self.path_version_win_regex if self._is_windows() else self.path_version_nix_regex m = path_version_regex.match(os.path.basename(path)) if m: return m[1] mlog.warning('Could not detect CUDA Toolkit version for {}'.format(path)) return '0.0' def _read_toolkit_version_txt(self, path): # Read 'version.txt' at the root of the CUDA Toolkit directory to determine the tookit version version_file_path = os.path.join(path, 'version.txt') try: with open(version_file_path) as version_file: version_str = version_file.readline() # e.g. 'CUDA Version 10.1.168' m = self.toolkit_version_regex.match(version_str) if m: return self._strip_patch_version(m[1]) except Exception as e: mlog.debug('Could not read CUDA Toolkit\'s version file {}: {}'.format(version_file_path, str(e))) return None @classmethod def _strip_patch_version(cls, version): return '.'.join(version.split('.')[:2]) def _detect_arch_libdir(self): arch = detect_cpu_family(self.env.coredata.compilers.host) machine = self.env.machines[self.for_machine] msg = '{} architecture is not supported in {} version of the CUDA Toolkit.' if machine.is_windows(): libdirs = {'x86': 'Win32', 'x86_64': 'x64'} if arch not in libdirs: raise DependencyException(msg.format(arch, 'Windows')) return os.path.join('lib', libdirs[arch]) elif machine.is_linux(): libdirs = {'x86_64': 'lib64', 'ppc64': 'lib'} if arch not in libdirs: raise DependencyException(msg.format(arch, 'Linux')) return libdirs[arch] elif machine.is_osx(): libdirs = {'x86_64': 'lib64'} if arch not in libdirs: raise DependencyException(msg.format(arch, 'macOS')) return libdirs[arch] else: raise DependencyException('CUDA Toolkit: unsupported platform.') def _find_requested_libraries(self): self.lib_modules = {} all_found = True for module in self.requested_modules: args = self.clib_compiler.find_library(module, self.env, [self.libdir] if self.libdir else []) if args is None: self._report_dependency_error('Couldn\'t find requested CUDA module \'{}\''.format(module)) all_found = False else: mlog.debug('Link args for CUDA module \'{}\' are {}'.format(module, args)) self.lib_modules[module] = args return all_found def _is_windows(self): return self.env.machines[self.for_machine].is_windows() def _report_dependency_error(self, msg, ret_val=None): if self.required: raise DependencyException(msg) mlog.debug(msg) return ret_val def log_details(self): module_str = ', '.join(self.requested_modules) return 'modules: ' + module_str def log_info(self): return self.cuda_path if self.cuda_path else '' def get_requested(self, kwargs): candidates = mesonlib.extract_as_list(kwargs, 'modules') for c in candidates: if not isinstance(c, str): raise DependencyException('CUDA module argument is not a string.') return candidates def get_link_args(self, **kwargs): args = [] if self.libdir: args += self.clib_compiler.get_linker_search_args(self.libdir) for lib in self.requested_modules: args += self.lib_modules[lib] return args