diff options
Diffstat (limited to '')
-rw-r--r-- | mesonbuild/dependencies/dub.py | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/mesonbuild/dependencies/dub.py b/mesonbuild/dependencies/dub.py new file mode 100644 index 0000000..ac2b667 --- /dev/null +++ b/mesonbuild/dependencies/dub.py @@ -0,0 +1,404 @@ +# Copyright 2013-2021 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. +from __future__ import annotations + +from .base import ExternalDependency, DependencyException, DependencyTypeName +from .pkgconfig import PkgConfigDependency +from ..mesonlib import (Popen_safe, OptionKey, join_args) +from ..programs import ExternalProgram +from .. import mlog +import re +import os +import json +import typing as T + +if T.TYPE_CHECKING: + from ..environment import Environment + +class DubDependency(ExternalDependency): + class_dubbin = None + + def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]): + 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 'required' in kwargs: + self.required = kwargs.get('required') + + if DubDependency.class_dubbin is None: + self.dubbin = self._check_dub() + DubDependency.class_dubbin = self.dubbin + else: + self.dubbin = DubDependency.class_dubbin + + if not self.dubbin: + if self.required: + raise DependencyException('DUB not found.') + self.is_found = False + return + + assert isinstance(self.dubbin, ExternalProgram) + mlog.debug('Determining dependency {!r} with DUB executable ' + '{!r}'.format(name, self.dubbin.get_path())) + + # if an explicit version spec was stated, use this when querying Dub + main_pack_spec = name + if 'version' in kwargs: + version_spec = kwargs['version'] + if isinstance(version_spec, list): + version_spec = " ".join(version_spec) + main_pack_spec = f'{name}@{version_spec}' + + # 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.get_option(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' + + # Ask dub for the package + describe_cmd = [ + 'describe', main_pack_spec, '--arch=' + dub_arch, + '--build=' + dub_buildtype, '--compiler=' + self.compiler.get_exelist()[-1] + ] + ret, res, err = self._call_dubbin(describe_cmd) + + if ret != 0: + mlog.debug('DUB describe failed: ' + err) + if 'locally' in err: + fetch_cmd = ['dub', 'fetch', main_pack_spec] + mlog.error(mlog.bold(main_pack_spec), 'is not present locally. You may try the following command:') + mlog.log(mlog.bold(join_args(fetch_cmd))) + self.is_found = False + return + + # A command that might be useful in case of missing DUB package + def dub_build_deep_command() -> str: + cmd = [ + 'dub', 'run', 'dub-build-deep', '--yes', '--', main_pack_spec, + '--arch=' + dub_arch, '--compiler=' + self.compiler.get_exelist()[-1], + '--build=' + dub_buildtype + ] + return join_args(cmd) + + dub_comp_id = self.compiler.get_id().replace('llvm', 'ldc').replace('gcc', 'gdc') + description = json.loads(res) + + self.compile_args = [] + self.link_args = self.raw_link_args = [] + + show_buildtype_warning = False + + def find_package_target(pkg: T.Dict[str, str]) -> 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_file, compatibilities) = self._find_compatible_package_target(description, pkg, 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(description['platform'].join('.'))) + 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(dub_build_deep_command())) + + 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 + self.is_found = False + packages = {} + for pkg in description['packages']: + packages[pkg['name']] = pkg + + if not pkg['active']: + continue + + if pkg['targetType'] == 'dynamicLibrary': + mlog.error('DUB dynamic library dependencies are not supported.') + self.is_found = False + return + + ## check that the main dependency is indeed a library + if pkg['name'] == name: + self.is_found = True + + if pkg['targetType'] not in ['library', 'sourceLibrary', 'staticLibrary']: + mlog.error(mlog.bold(name), "found but it isn't a library") + self.is_found = False + return + + self.version = pkg['version'] + self.pkg = pkg + + # collect all targets + targets = {} + for tgt in description['targets']: + targets[tgt['rootPackage']] = tgt + + if name not in targets: + self.is_found = False + 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(main_pack_spec)) + + if not self.is_found: + mlog.error(f'Could not find {name} in DUB description') + return + + # Current impl only supports static libraries + self.static = True + + # 2 + if not find_package_target(self.pkg): + self.is_found = False + return + + # 3 + for link_dep in targets[name]['linkDependencies']: + pkg = packages[link_dep] + if not find_package_target(pkg): + self.is_found = False + 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(dub_build_deep_command())) + + # 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) + + # 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_compatible_package_target(self, jdesc: T.Dict[str, str], jpack: T.Dict[str, str], dub_comp_id: str) -> T.Tuple[str, T.Set[str]]: + dub_build_path = os.path.join(jpack['path'], '.dub', 'build') + + if not os.path.exists(dub_build_path): + return (None, None) + + # try to find a dir 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? + + conf = jpack['configuration'] + build_type = jdesc['buildType'] + platforms = jdesc['platform'] + archs = jdesc['architecture'] + + # Get D frontend version implemented in the compiler, or the compiler version itself + # gdc doesn't support this + comp_versions = [] + + if dub_comp_id != 'gdc': + comp_versions.append(self.compiler.version) + + ret, res = self._call_compbin(['--version'])[0:2] + if ret != 0: + mlog.error('Failed to run {!r}', mlog.bold(dub_comp_id)) + return (None, None) + 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]) + + compatibilities: T.Set[str] = set() + + # 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') + + for entry in os.listdir(dub_build_path): + + target = os.path.join(dub_build_path, entry, jpack['targetFileName']) + 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 = set() + + if conf in entry: + comps.add('configuration') + + if build_type in entry: + comps.add('build_type') + + if all(platform in entry for platform in platforms): + comps.add('platform') + + if all(arch in entry for arch in archs): + comps.add('arch') + + if dub_comp_id in entry: + comps.add('compiler') + + if dub_comp_id == 'gdc' or any(cv in entry for cv in comp_versions): + comps.add('compiler_version') + + if all(key in comps for key in check_list): + return (target, comps) + else: + compatibilities = set.union(compatibilities, comps) + + return (None, compatibilities) + + 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) + 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.Union[bool, ExternalProgram]: + dubbin: T.Union[bool, ExternalProgram] = ExternalProgram('dub', silent=True) + assert isinstance(dubbin, ExternalProgram) + if dubbin.found(): + 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()))) + # Set to False instead of None to signify that we've already + # searched for it and not found it + dubbin = False + except (FileNotFoundError, PermissionError): + dubbin = False + else: + dubbin = False + if isinstance(dubbin, ExternalProgram): + mlog.log('Found DUB:', mlog.bold(dubbin.get_path()), + '(%s)' % out.strip()) + else: + mlog.log('Found DUB:', mlog.red('NO')) + return dubbin |