# SPDX-License-Identifier: Apache-2.0 # Copyright 2013-2021 The Meson development team from __future__ import annotations from .base import ExternalDependency, DependencyException, DependencyTypeName from .pkgconfig import PkgConfigDependency from ..mesonlib import (Popen_safe, join_args, version_compare, version_compare_many) from ..options import OptionKey from ..programs import ExternalProgram from .. import mlog from enum import Enum import re import os import json import typing as T if T.TYPE_CHECKING: from typing_extensions import TypedDict from ..environment import Environment from .base import DependencyObjectKWs # Definition of what `dub describe` returns (only the fields used by Meson) class DubDescription(TypedDict): platform: T.List[str] architecture: T.List[str] buildType: str packages: T.List[DubPackDesc] targets: T.List[DubTargetDesc] class DubPackDesc(TypedDict): name: str version: str active: bool configuration: str path: str targetType: str targetFileName: str class DubTargetDesc(TypedDict): rootPackage: str linkDependencies: T.List[str] buildSettings: DubBuildSettings cacheArtifactPath: str class DubBuildSettings(TypedDict): importPaths: T.List[str] stringImportPaths: T.List[str] versions: T.List[str] mainSourceFile: str sourceFiles: T.List[str] dflags: T.List[str] libs: T.List[str] lflags: T.List[str] class FindTargetEntry(TypedDict): search: str artifactPath: str class DubDescriptionSource(Enum): Local = 'local' External = 'external' class DubDependency(ExternalDependency): # dub program and version class_dubbin: T.Optional[T.Tuple[ExternalProgram, str]] = None class_dubbin_searched = False class_cache_dir = '' # Map Meson Compiler ID's to Dub Compiler ID's _ID_MAP: T.Mapping[str, str] = { 'dmd': 'dmd', 'gcc': 'gdc', 'llvm': 'ldc', } def __init__(self, name: str, environment: 'Environment', kwargs: DependencyObjectKWs): super().__init__(DependencyTypeName('dub'), environment, kwargs, language='d') self.name = name from ..compilers.d import DCompiler, d_feature_args _temp_comp = super().get_compiler() assert isinstance(_temp_comp, DCompiler) self.compiler = _temp_comp if kwargs.get('required') is not None: self.required = kwargs['required'] if DubDependency.class_dubbin is None and not DubDependency.class_dubbin_searched: DubDependency.class_dubbin = self._check_dub() DubDependency.class_dubbin_searched = True if DubDependency.class_dubbin is None: if self.required: raise DependencyException('DUB not found.') return (self.dubbin, dubver) = DubDependency.class_dubbin # pylint: disable=unpacking-non-sequence assert isinstance(self.dubbin, ExternalProgram) # Check Dub's compatibility with Meson self._search_in_cache = version_compare(dubver, '<=1.31.1') self._use_cache_describe = version_compare(dubver, '>=1.35.0') self._dub_has_build_deep = version_compare(dubver, '>=1.35.0') if not self._search_in_cache and not self._use_cache_describe: if self.required: raise DependencyException( f'DUB version {dubver} is not compatible with Meson' " (can't locate artifacts in DUB's cache). Upgrade to Dub >= 1.35.") else: mlog.warning(f'DUB dependency {name} not found because Dub {dubver} ' "is not compatible with Meson. (Can't locate artifacts in DUB's cache)." ' Upgrade to Dub >= 1.35') return mlog.debug('Determining dependency {!r} with DUB executable ' '{!r}'.format(name, self.dubbin.get_path())) # we need to know the target architecture dub_arch = self.compiler.arch # we need to know the build type as well dub_buildtype = str(environment.coredata.optstore.get_value_for(OptionKey('buildtype'))) # MESON types: choices=['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom'])), # DUB types: debug (default), plain, release, release-debug, release-nobounds, unittest, profile, profile-gc, # docs, ddox, cov, unittest-cov, syntax and custom if dub_buildtype == 'debugoptimized': dub_buildtype = 'release-debug' elif dub_buildtype == 'minsize': dub_buildtype = 'release' result = self._get_dub_description(dub_arch, dub_buildtype) if result is None: return description, build_cmd, description_source = result dub_comp_id = self._ID_MAP[self.compiler.get_id()] self.compile_args = [] self.link_args = self.raw_link_args = [] show_buildtype_warning = False # collect all targets targets = {t['rootPackage']: t for t in description['targets']} def find_package_target(pkg: DubPackDesc) -> bool: nonlocal show_buildtype_warning # try to find a static library in a DUB folder corresponding to # version, configuration, compiler, arch and build-type # if can find, add to link_args. # link_args order is meaningful, so this function MUST be called in the right order pack_id = f'{pkg["name"]}@{pkg["version"]}' tgt_desc = targets[pkg['name']] (tgt_file, compatibilities) = self._find_target_in_cache(description, pkg, tgt_desc, dub_comp_id) if tgt_file is None: if not compatibilities: mlog.error(mlog.bold(pack_id), 'not found') elif 'compiler' not in compatibilities: mlog.error(mlog.bold(pack_id), 'found but not compiled with ', mlog.bold(dub_comp_id)) elif dub_comp_id != 'gdc' and 'compiler_version' not in compatibilities: mlog.error(mlog.bold(pack_id), 'found but not compiled with', mlog.bold(f'{dub_comp_id}-{self.compiler.version}')) elif 'arch' not in compatibilities: mlog.error(mlog.bold(pack_id), 'found but not compiled for', mlog.bold(dub_arch)) elif 'platform' not in compatibilities: mlog.error(mlog.bold(pack_id), 'found but not compiled for', mlog.bold('.'.join(description['platform']))) elif 'configuration' not in compatibilities: mlog.error(mlog.bold(pack_id), 'found but not compiled for the', mlog.bold(pkg['configuration']), 'configuration') else: mlog.error(mlog.bold(pack_id), 'not found') mlog.log('You may try the following command to install the necessary DUB libraries:') mlog.log(mlog.bold(build_cmd)) return False if 'build_type' not in compatibilities: mlog.warning(mlog.bold(pack_id), 'found but not compiled as', mlog.bold(dub_buildtype)) show_buildtype_warning = True self.link_args.append(tgt_file) return True # Main algorithm: # 1. Ensure that the target is a compatible library type (not dynamic) # 2. Find a compatible built library for the main dependency # 3. Do the same for each sub-dependency. # link_args MUST be in the same order than the "linkDependencies" of the main target # 4. Add other build settings (imports, versions etc.) # 1 packages: T.Dict[str, DubPackDesc] = {} found_it = False for pkg in description['packages']: packages[pkg['name']] = pkg if not pkg['active']: continue # check that the main dependency is indeed a library if pkg['name'] == name: if pkg['targetType'] not in ['library', 'sourceLibrary', 'staticLibrary']: mlog.error(mlog.bold(name), "found but it isn't a static library, it is:", pkg['targetType']) return if self.version_reqs is not None: ver = pkg['version'] if not version_compare_many(ver, self.version_reqs)[0]: mlog.error(mlog.bold(f'{name}@{ver}'), 'does not satisfy all version requirements of:', ' '.join(self.version_reqs)) return found_it = True self.version = pkg['version'] self.pkg = pkg if not found_it: mlog.error(f'Could not find {name} in DUB description.') if description_source is DubDescriptionSource.Local: mlog.log('Make sure that the dependency is registered for your dub project by running:') mlog.log(mlog.bold(f'dub add {name}')) elif description_source is DubDescriptionSource.External: # `dub describe pkg` did not contain the pkg raise RuntimeError(f'`dub describe` succeeded but it does not contains {name}') return if name not in targets: if self.pkg['targetType'] == 'sourceLibrary': # source libraries have no associated targets, # but some build settings like import folders must be found from the package object. # Current algo only get these from "buildSettings" in the target object. # Let's save this for a future PR. # (See openssl DUB package for example of sourceLibrary) mlog.error('DUB targets of type', mlog.bold('sourceLibrary'), 'are not supported.') else: mlog.error('Could not find target description for', mlog.bold(self.name)) return # Current impl only supports static libraries self.static = True # 2 if not find_package_target(self.pkg): return # 3 for link_dep in targets[name]['linkDependencies']: pkg = packages[link_dep] if not find_package_target(pkg): return if show_buildtype_warning: mlog.log('If it is not suitable, try the following command and reconfigure Meson with', mlog.bold('--clearcache')) mlog.log(mlog.bold(build_cmd)) # 4 bs = targets[name]['buildSettings'] for flag in bs['dflags']: self.compile_args.append(flag) for path in bs['importPaths']: self.compile_args.append('-I' + path) for path in bs['stringImportPaths']: if 'import_dir' not in d_feature_args[self.compiler.id]: break flag = d_feature_args[self.compiler.id]['import_dir'] self.compile_args.append(f'{flag}={path}') for ver in bs['versions']: if 'version' not in d_feature_args[self.compiler.id]: break flag = d_feature_args[self.compiler.id]['version'] self.compile_args.append(f'{flag}={ver}') if bs['mainSourceFile']: self.compile_args.append(bs['mainSourceFile']) # pass static libraries # linkerFiles are added during step 3 # for file in bs['linkerFiles']: # self.link_args.append(file) for file in bs['sourceFiles']: # sourceFiles may contain static libraries if file.endswith('.lib') or file.endswith('.a'): self.link_args.append(file) for flag in bs['lflags']: self.link_args.append(flag) is_windows = self.env.machines.host.is_windows() if is_windows: winlibs = ['kernel32', 'user32', 'gdi32', 'winspool', 'shell32', 'ole32', 'oleaut32', 'uuid', 'comdlg32', 'advapi32', 'ws2_32'] for lib in bs['libs']: if os.name != 'nt': # trying to add system libraries by pkg-config pkgdep = PkgConfigDependency(lib, environment, {'required': True, 'silent': True}) if pkgdep.is_found: for arg in pkgdep.get_compile_args(): self.compile_args.append(arg) for arg in pkgdep.get_link_args(): self.link_args.append(arg) for arg in pkgdep.get_link_args(raw=True): self.raw_link_args.append(arg) continue if is_windows and lib in winlibs: self.link_args.append(lib + '.lib') continue # fallback self.link_args.append('-l'+lib) self.is_found = True # Get the dub description needed to resolve the dependency and a # build command that can be used to build the dependency in case it is # not present. def _get_dub_description(self, dub_arch: str, dub_buildtype: str) -> T.Optional[T.Tuple[DubDescription, str, DubDescriptionSource]]: def get_build_command() -> T.List[str]: if self._dub_has_build_deep: cmd = ['dub', 'build', '--deep'] else: cmd = ['dub', 'run', '--yes', 'dub-build-deep', '--'] return cmd + [ '--arch=' + dub_arch, '--compiler=' + self.compiler.get_exelist()[-1], '--build=' + dub_buildtype, ] # Ask dub for the package describe_cmd = [ 'describe', '--arch=' + dub_arch, '--build=' + dub_buildtype, '--compiler=' + self.compiler.get_exelist()[-1] ] helper_build = join_args(get_build_command()) source = DubDescriptionSource.Local ret, res, err = self._call_dubbin(describe_cmd) if ret == 0: return (json.loads(res), helper_build, source) pack_spec = self.name if self.version_reqs is not None: if len(self.version_reqs) > 1: mlog.error('Multiple version requirements are not supported for raw dub dependencies.') mlog.error("Please specify only an exact version like '1.2.3'") raise DependencyException('Multiple version requirements are not solvable for raw dub depencies') elif len(self.version_reqs) == 1: pack_spec += '@' + self.version_reqs[0] describe_cmd = [ 'describe', pack_spec, '--arch=' + dub_arch, '--build=' + dub_buildtype, '--compiler=' + self.compiler.get_exelist()[-1] ] helper_build = join_args(get_build_command() + [pack_spec]) source = DubDescriptionSource.External ret, res, err = self._call_dubbin(describe_cmd) if ret == 0: return (json.loads(res), helper_build, source) mlog.debug('DUB describe failed: ' + err) if 'locally' in err: mlog.error(mlog.bold(pack_spec), 'is not present locally. You may try the following command:') mlog.log(mlog.bold(helper_build)) return None # This function finds the target of the provided JSON package, built for the right # compiler, architecture, configuration... # It returns (target|None, {compatibilities}) # If None is returned for target, compatibilities will list what other targets were found without full compatibility def _find_target_in_cache(self, desc: DubDescription, pkg_desc: DubPackDesc, tgt_desc: DubTargetDesc, dub_comp_id: str ) -> T.Tuple[T.Optional[str], T.Set[str]]: mlog.debug('Searching in DUB cache for compatible', pkg_desc['targetFileName']) # recent DUB versions include a direct path to a compatible cached artifact if self._use_cache_describe: tgt_file = tgt_desc['cacheArtifactPath'] if os.path.exists(tgt_file): return (tgt_file, {'configuration', 'platform', 'arch', 'compiler', 'compiler_version', 'build_type'}) else: return (None, set()) assert self._search_in_cache # try to find a string like library-debug-linux.posix-x86_64-ldc_2081-EF934983A3319F8F8FF2F0E107A363BA # fields are: # - configuration # - build type # - platform # - architecture # - compiler id (dmd, ldc, gdc) # - compiler version or frontend id or frontend version? comp_versions = self._get_comp_versions_to_find(dub_comp_id) # build_type is not in check_list because different build types might be compatible. # We do show a WARNING that the build type is not the same. # It might be critical in release builds, and acceptable otherwise check_list = {'configuration', 'platform', 'arch', 'compiler', 'compiler_version'} compatibilities: T.Set[str] = set() for entry in self._cache_entries(pkg_desc): target = entry['artifactPath'] if not os.path.exists(target): # unless Dub and Meson are racing, the target file should be present # when the directory is present mlog.debug("WARNING: Could not find a Dub target: " + target) continue # we build a new set for each entry, because if this target is returned # we want to return only the compatibilities associated to this target # otherwise we could miss the WARNING about build_type comps: T.Set[str] = set() search = entry['search'] mlog.debug('searching compatibility in ' + search) mlog.debug('compiler_versions', comp_versions) if pkg_desc['configuration'] in search: comps.add('configuration') if desc['buildType'] in search: comps.add('build_type') if all(platform in search for platform in desc['platform']): comps.add('platform') if all(arch in search for arch in desc['architecture']): comps.add('arch') if dub_comp_id in search: comps.add('compiler') if not comp_versions or any(cv in search for cv in comp_versions): comps.add('compiler_version') if check_list.issubset(comps): mlog.debug('Found', target) return (target, comps) else: compatibilities = set.union(compatibilities, comps) return (None, compatibilities) def _cache_entries(self, pkg_desc: DubPackDesc) -> T.List[FindTargetEntry]: # the "old" cache is the `.dub` directory in every package of ~/.dub/packages dub_build_path = os.path.join(pkg_desc['path'], '.dub', 'build') if not os.path.exists(dub_build_path): mlog.warning('No such cache folder:', dub_build_path) return [] mlog.debug('Checking in DUB cache folder', dub_build_path) return [ { 'search': dir_entry, 'artifactPath': os.path.join(dub_build_path, dir_entry, pkg_desc['targetFileName']) } for dir_entry in os.listdir(dub_build_path) ] def _get_comp_versions_to_find(self, dub_comp_id: str) -> T.List[str]: # Get D frontend version implemented in the compiler, or the compiler version itself # gdc doesn't support this if dub_comp_id == 'gdc': return [] comp_versions = [self.compiler.version] ret, res = self._call_compbin(['--version'])[0:2] if ret != 0: mlog.error('Failed to run', mlog.bold(' '.join(self.dubbin.get_command() + ['--version']))) return [] d_ver_reg = re.search('v[0-9].[0-9][0-9][0-9].[0-9]', res) # Ex.: v2.081.2 if d_ver_reg is not None: frontend_version = d_ver_reg.group() frontend_id = frontend_version.rsplit('.', 1)[0].replace( 'v', '').replace('.', '') # Fix structure. Ex.: 2081 comp_versions.extend([frontend_version, frontend_id]) return comp_versions def _call_dubbin(self, args: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str, str]: assert isinstance(self.dubbin, ExternalProgram) p, out, err = Popen_safe(self.dubbin.get_command() + args, env=env, cwd=self.env.get_source_dir()) return p.returncode, out.strip(), err.strip() def _call_compbin(self, args: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str, str]: p, out, err = Popen_safe(self.compiler.get_exelist() + args, env=env) return p.returncode, out.strip(), err.strip() def _check_dub(self) -> T.Optional[T.Tuple[ExternalProgram, str]]: def find() -> T.Optional[T.Tuple[ExternalProgram, str]]: dubbin = ExternalProgram('dub', silent=True) if not dubbin.found(): return None try: p, out = Popen_safe(dubbin.get_command() + ['--version'])[0:2] if p.returncode != 0: mlog.warning('Found dub {!r} but couldn\'t run it' ''.format(' '.join(dubbin.get_command()))) return None except (FileNotFoundError, PermissionError): return None vermatch = re.search(r'DUB version (\d+\.\d+\.\d+.*), ', out.strip()) if vermatch: dubver = vermatch.group(1) else: mlog.warning(f"Found dub {' '.join(dubbin.get_command())} but couldn't parse version in {out.strip()}") return None return (dubbin, dubver) found = find() if found is None: mlog.log('Found DUB:', mlog.red('NO')) else: (dubbin, dubver) = found mlog.log('Found DUB:', mlog.bold(dubbin.get_path()), '(version %s)' % dubver) return found