1# Copyright 2013-2017 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
15# This file contains the detection logic for external dependencies that
16# are UI-related.
17import os
18import subprocess
19import typing as T
20
21from .. import mlog
22from .. import mesonlib
23from ..mesonlib import (
24    Popen_safe, extract_as_list, version_compare_many
25)
26from ..environment import detect_cpu_family
27
28from .base import DependencyException, DependencyMethods, DependencyTypeName, SystemDependency
29from .configtool import ConfigToolDependency
30from .factory import DependencyFactory
31
32if T.TYPE_CHECKING:
33    from ..environment import Environment
34
35
36class GLDependencySystem(SystemDependency):
37    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
38        super().__init__(name, environment, kwargs)
39
40        if self.env.machines[self.for_machine].is_darwin():
41            self.is_found = True
42            # FIXME: Use AppleFrameworks dependency
43            self.link_args = ['-framework', 'OpenGL']
44            # FIXME: Detect version using self.clib_compiler
45            return
46        if self.env.machines[self.for_machine].is_windows():
47            self.is_found = True
48            # FIXME: Use self.clib_compiler.find_library()
49            self.link_args = ['-lopengl32']
50            # FIXME: Detect version using self.clib_compiler
51            return
52
53    @staticmethod
54    def get_methods() -> T.List[DependencyMethods]:
55        if mesonlib.is_osx() or mesonlib.is_windows():
56            return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]
57        else:
58            return [DependencyMethods.PKGCONFIG]
59
60    def log_tried(self) -> str:
61        return 'system'
62
63class GnuStepDependency(ConfigToolDependency):
64
65    tools = ['gnustep-config']
66    tool_name = 'gnustep-config'
67
68    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
69        super().__init__('gnustep', environment, kwargs, language='objc')
70        if not self.is_found:
71            return
72        self.modules = kwargs.get('modules', [])
73        self.compile_args = self.filter_args(
74            self.get_config_value(['--objc-flags'], 'compile_args'))
75        self.link_args = self.weird_filter(self.get_config_value(
76            ['--gui-libs' if 'gui' in self.modules else '--base-libs'],
77            'link_args'))
78
79    def find_config(self, versions: T.Optional[T.List[str]] = None, returncode: int = 0) -> T.Tuple[T.Optional[T.List[str]], T.Optional[str]]:
80        tool = [self.tools[0]]
81        try:
82            p, out = Popen_safe(tool + ['--help'])[:2]
83        except (FileNotFoundError, PermissionError):
84            return (None, None)
85        if p.returncode != returncode:
86            return (None, None)
87        self.config = tool
88        found_version = self.detect_version()
89        if versions and not version_compare_many(found_version, versions)[0]:
90            return (None, found_version)
91
92        return (tool, found_version)
93
94    @staticmethod
95    def weird_filter(elems: T.List[str]) -> T.List[str]:
96        """When building packages, the output of the enclosing Make is
97        sometimes mixed among the subprocess output. I have no idea why. As a
98        hack filter out everything that is not a flag.
99        """
100        return [e for e in elems if e.startswith('-')]
101
102    @staticmethod
103    def filter_args(args: T.List[str]) -> T.List[str]:
104        """gnustep-config returns a bunch of garbage args such as -O2 and so
105        on. Drop everything that is not needed.
106        """
107        result = []
108        for f in args:
109            if f.startswith('-D') \
110                    or f.startswith('-f') \
111                    or f.startswith('-I') \
112                    or f == '-pthread' \
113                    or (f.startswith('-W') and not f == '-Wall'):
114                result.append(f)
115        return result
116
117    def detect_version(self) -> str:
118        gmake = self.get_config_value(['--variable=GNUMAKE'], 'variable')[0]
119        makefile_dir = self.get_config_value(['--variable=GNUSTEP_MAKEFILES'], 'variable')[0]
120        # This Makefile has the GNUStep version set
121        base_make = os.path.join(makefile_dir, 'Additional', 'base.make')
122        # Print the Makefile variable passed as the argument. For instance, if
123        # you run the make target `print-SOME_VARIABLE`, this will print the
124        # value of the variable `SOME_VARIABLE`.
125        printver = "print-%:\n\t@echo '$($*)'"
126        env = os.environ.copy()
127        # See base.make to understand why this is set
128        env['FOUNDATION_LIB'] = 'gnu'
129        p, o, e = Popen_safe([gmake, '-f', '-', '-f', base_make,
130                              'print-GNUSTEP_BASE_VERSION'],
131                             env=env, write=printver, stdin=subprocess.PIPE)
132        version = o.strip()
133        if not version:
134            mlog.debug("Couldn't detect GNUStep version, falling back to '1'")
135            # Fallback to setting some 1.x version
136            version = '1'
137        return version
138
139
140class SDL2DependencyConfigTool(ConfigToolDependency):
141
142    tools = ['sdl2-config']
143    tool_name = 'sdl2-config'
144
145    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
146        super().__init__(name, environment, kwargs)
147        if not self.is_found:
148            return
149        self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
150        self.link_args = self.get_config_value(['--libs'], 'link_args')
151
152    @staticmethod
153    def get_methods() -> T.List[DependencyMethods]:
154        if mesonlib.is_osx():
155            return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK]
156        else:
157            return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL]
158
159
160class WxDependency(ConfigToolDependency):
161
162    tools = ['wx-config-3.0', 'wx-config', 'wx-config-gtk3']
163    tool_name = 'wx-config'
164
165    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
166        super().__init__('WxWidgets', environment, kwargs, language='cpp')
167        if not self.is_found:
168            return
169        self.requested_modules = self.get_requested(kwargs)
170
171        extra_args = []
172        if self.static:
173            extra_args.append('--static=yes')
174
175            # Check to make sure static is going to work
176            err = Popen_safe(self.config + extra_args)[2]
177            if 'No config found to match' in err:
178                mlog.debug('WxWidgets is missing static libraries.')
179                self.is_found = False
180                return
181
182        # wx-config seems to have a cflags as well but since it requires C++,
183        # this should be good, at least for now.
184        self.compile_args = self.get_config_value(['--cxxflags'] + extra_args + self.requested_modules, 'compile_args')
185        self.link_args = self.get_config_value(['--libs'] + extra_args + self.requested_modules, 'link_args')
186
187    @staticmethod
188    def get_requested(kwargs: T.Dict[str, T.Any]) -> T.List[str]:
189        if 'modules' not in kwargs:
190            return []
191        candidates = extract_as_list(kwargs, 'modules')
192        for c in candidates:
193            if not isinstance(c, str):
194                raise DependencyException('wxwidgets module argument is not a string')
195        return candidates
196
197
198class VulkanDependencySystem(SystemDependency):
199
200    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
201        super().__init__(name, environment, kwargs, language=language)
202
203        try:
204            self.vulkan_sdk = os.environ['VULKAN_SDK']
205            if not os.path.isabs(self.vulkan_sdk):
206                raise DependencyException('VULKAN_SDK must be an absolute path.')
207        except KeyError:
208            self.vulkan_sdk = None
209
210        if self.vulkan_sdk:
211            # TODO: this config might not work on some platforms, fix bugs as reported
212            # we should at least detect other 64-bit platforms (e.g. armv8)
213            lib_name = 'vulkan'
214            lib_dir = 'lib'
215            inc_dir = 'include'
216            if mesonlib.is_windows():
217                lib_name = 'vulkan-1'
218                lib_dir = 'Lib32'
219                inc_dir = 'Include'
220                if detect_cpu_family(self.env.coredata.compilers.host) == 'x86_64':
221                    lib_dir = 'Lib'
222
223            # make sure header and lib are valid
224            inc_path = os.path.join(self.vulkan_sdk, inc_dir)
225            header = os.path.join(inc_path, 'vulkan', 'vulkan.h')
226            lib_path = os.path.join(self.vulkan_sdk, lib_dir)
227            find_lib = self.clib_compiler.find_library(lib_name, environment, [lib_path])
228
229            if not find_lib:
230                raise DependencyException('VULKAN_SDK point to invalid directory (no lib)')
231
232            if not os.path.isfile(header):
233                raise DependencyException('VULKAN_SDK point to invalid directory (no include)')
234
235            # XXX: this is very odd, and may deserve being removed
236            self.type_name = DependencyTypeName('vulkan_sdk')
237            self.is_found = True
238            self.compile_args.append('-I' + inc_path)
239            self.link_args.append('-L' + lib_path)
240            self.link_args.append('-l' + lib_name)
241
242            # TODO: find a way to retrieve the version from the sdk?
243            # Usually it is a part of the path to it (but does not have to be)
244            return
245        else:
246            # simply try to guess it, usually works on linux
247            libs = self.clib_compiler.find_library('vulkan', environment, [])
248            if libs is not None and self.clib_compiler.has_header('vulkan/vulkan.h', '', environment, disable_cache=True)[0]:
249                self.is_found = True
250                for lib in libs:
251                    self.link_args.append(lib)
252                return
253
254    @staticmethod
255    def get_methods() -> T.List[DependencyMethods]:
256        return [DependencyMethods.SYSTEM]
257
258    def log_tried(self) -> str:
259        return 'system'
260
261gl_factory = DependencyFactory(
262    'gl',
263    [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
264    system_class=GLDependencySystem,
265)
266
267sdl2_factory = DependencyFactory(
268    'sdl2',
269    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK],
270    configtool_class=SDL2DependencyConfigTool,
271)
272
273vulkan_factory = DependencyFactory(
274    'vulkan',
275    [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
276    system_class=VulkanDependencySystem,
277)
278