1# Copyright 2013-2017 The Meson development team
2# Copyright © 2021 Intel Corporation
3# SPDX-license-identifier: Apache-2.0
4
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8
9#     http://www.apache.org/licenses/LICENSE-2.0
10
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Dependency finders for the Qt framework."""
18
19import abc
20import re
21import os
22import typing as T
23
24from .base import DependencyException, DependencyMethods
25from .configtool import ConfigToolDependency
26from .framework import ExtraFrameworkDependency
27from .pkgconfig import PkgConfigDependency
28from .factory import DependencyFactory
29from .. import mlog
30from .. import mesonlib
31
32if T.TYPE_CHECKING:
33    from ..compilers import Compiler
34    from ..envconfig import MachineInfo
35    from ..environment import Environment
36
37
38def _qt_get_private_includes(mod_inc_dir: str, module: str, mod_version: str) -> T.List[str]:
39    # usually Qt5 puts private headers in /QT_INSTALL_HEADERS/module/VERSION/module/private
40    # except for at least QtWebkit and Enginio where the module version doesn't match Qt version
41    # as an example with Qt 5.10.1 on linux you would get:
42    # /usr/include/qt5/QtCore/5.10.1/QtCore/private/
43    # /usr/include/qt5/QtWidgets/5.10.1/QtWidgets/private/
44    # /usr/include/qt5/QtWebKit/5.212.0/QtWebKit/private/
45
46    # on Qt4 when available private folder is directly in module folder
47    # like /usr/include/QtCore/private/
48    if int(mod_version.split('.')[0]) < 5:
49        return []
50
51    private_dir = os.path.join(mod_inc_dir, mod_version)
52    # fallback, let's try to find a directory with the latest version
53    if not os.path.exists(private_dir):
54        dirs = [filename for filename in os.listdir(mod_inc_dir)
55                if os.path.isdir(os.path.join(mod_inc_dir, filename))]
56
57        for dirname in sorted(dirs, reverse=True):
58            if len(dirname.split('.')) == 3:
59                private_dir = dirname
60                break
61    return [private_dir, os.path.join(private_dir, 'Qt' + module)]
62
63
64def get_qmake_host_bins(qvars: T.Dict[str, str]) -> str:
65    # Prefer QT_HOST_BINS (qt5, correct for cross and native compiling)
66    # but fall back to QT_INSTALL_BINS (qt4)
67    if 'QT_HOST_BINS' in qvars:
68        return qvars['QT_HOST_BINS']
69    return qvars['QT_INSTALL_BINS']
70
71
72def _get_modules_lib_suffix(version: str, info: 'MachineInfo', is_debug: bool) -> str:
73    """Get the module suffix based on platform and debug type."""
74    suffix = ''
75    if info.is_windows():
76        if is_debug:
77            suffix += 'd'
78        if version.startswith('4'):
79            suffix += '4'
80    if info.is_darwin():
81        if is_debug:
82            suffix += '_debug'
83    if mesonlib.version_compare(version, '>= 5.14.0'):
84        if info.is_android():
85            if info.cpu_family == 'x86':
86                suffix += '_x86'
87            elif info.cpu_family == 'x86_64':
88                suffix += '_x86_64'
89            elif info.cpu_family == 'arm':
90                suffix += '_armeabi-v7a'
91            elif info.cpu_family == 'aarch64':
92                suffix += '_arm64-v8a'
93            else:
94                mlog.warning(f'Android target arch "{info.cpu_family}"" for Qt5 is unknown, '
95                             'module detection may not work')
96    return suffix
97
98
99class QtExtraFrameworkDependency(ExtraFrameworkDependency):
100    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
101        super().__init__(name, env, kwargs, language=language)
102        self.mod_name = name[2:]
103
104    def get_compile_args(self, with_private_headers: bool = False, qt_version: str = "0") -> T.List[str]:
105        if self.found():
106            mod_inc_dir = os.path.join(self.framework_path, 'Headers')
107            args = ['-I' + mod_inc_dir]
108            if with_private_headers:
109                args += ['-I' + dirname for dirname in _qt_get_private_includes(mod_inc_dir, self.mod_name, qt_version)]
110            return args
111        return []
112
113
114class _QtBase:
115
116    """Mixin class for shared components between PkgConfig and Qmake."""
117
118    link_args: T.List[str]
119    clib_compiler: 'Compiler'
120    env: 'Environment'
121
122    def __init__(self, name: str, kwargs: T.Dict[str, T.Any]):
123        self.qtname = name.capitalize()
124        self.qtver = name[-1]
125        if self.qtver == "4":
126            self.qtpkgname = 'Qt'
127        else:
128            self.qtpkgname = self.qtname
129
130        self.private_headers = T.cast(bool, kwargs.get('private_headers', False))
131
132        self.requested_modules = mesonlib.stringlistify(mesonlib.extract_as_list(kwargs, 'modules'))
133        if not self.requested_modules:
134            raise DependencyException('No ' + self.qtname + '  modules specified.')
135
136        self.qtmain = T.cast(bool, kwargs.get('main', False))
137        if not isinstance(self.qtmain, bool):
138            raise DependencyException('"main" argument must be a boolean')
139
140    def _link_with_qtmain(self, is_debug: bool, libdir: T.Union[str, T.List[str]]) -> bool:
141        libdir = mesonlib.listify(libdir)  # TODO: shouldn't be necessary
142        base_name = 'qtmaind' if is_debug else 'qtmain'
143        qtmain = self.clib_compiler.find_library(base_name, self.env, libdir)
144        if qtmain:
145            self.link_args.append(qtmain[0])
146            return True
147        return False
148
149    def get_exe_args(self, compiler: 'Compiler') -> T.List[str]:
150        # Originally this was -fPIE but nowadays the default
151        # for upstream and distros seems to be -reduce-relocations
152        # which requires -fPIC. This may cause a performance
153        # penalty when using self-built Qt or on platforms
154        # where -fPIC is not required. If this is an issue
155        # for you, patches are welcome.
156        return compiler.get_pic_args()
157
158    def log_details(self) -> str:
159        return f'modules: {", ".join(sorted(self.requested_modules))}'
160
161
162class QtPkgConfigDependency(_QtBase, PkgConfigDependency, metaclass=abc.ABCMeta):
163
164    """Specialization of the PkgConfigDependency for Qt."""
165
166    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
167        _QtBase.__init__(self, name, kwargs)
168
169        # Always use QtCore as the "main" dependency, since it has the extra
170        # pkg-config variables that a user would expect to get. If "Core" is
171        # not a requested module, delete the compile and link arguments to
172        # avoid linking with something they didn't ask for
173        PkgConfigDependency.__init__(self, self.qtpkgname + 'Core', env, kwargs)
174        if 'Core' not in self.requested_modules:
175            self.compile_args = []
176            self.link_args = []
177
178        for m in self.requested_modules:
179            mod = PkgConfigDependency(self.qtpkgname + m, self.env, kwargs, language=self.language)
180            if not mod.found():
181                self.is_found = False
182                return
183            if self.private_headers:
184                qt_inc_dir = mod.get_pkgconfig_variable('includedir', {})
185                mod_private_dir = os.path.join(qt_inc_dir, 'Qt' + m)
186                if not os.path.isdir(mod_private_dir):
187                    # At least some versions of homebrew don't seem to set this
188                    # up correctly. /usr/local/opt/qt/include/Qt + m_name is a
189                    # symlink to /usr/local/opt/qt/include, but the pkg-config
190                    # file points to /usr/local/Cellar/qt/x.y.z/Headers/, and
191                    # the Qt + m_name there is not a symlink, it's a file
192                    mod_private_dir = qt_inc_dir
193                mod_private_inc = _qt_get_private_includes(mod_private_dir, m, mod.version)
194                for directory in mod_private_inc:
195                    mod.compile_args.append('-I' + directory)
196            self._add_sub_dependency([lambda: mod])
197
198        if self.env.machines[self.for_machine].is_windows() and self.qtmain:
199            # Check if we link with debug binaries
200            debug_lib_name = self.qtpkgname + 'Core' + _get_modules_lib_suffix(self.version, self.env.machines[self.for_machine], True)
201            is_debug = False
202            for arg in self.get_link_args():
203                if arg == f'-l{debug_lib_name}' or arg.endswith(f'{debug_lib_name}.lib') or arg.endswith(f'{debug_lib_name}.a'):
204                    is_debug = True
205                    break
206            libdir = self.get_pkgconfig_variable('libdir', {})
207            if not self._link_with_qtmain(is_debug, libdir):
208                self.is_found = False
209                return
210
211        self.bindir = self.get_pkgconfig_host_bins(self)
212        if not self.bindir:
213            # If exec_prefix is not defined, the pkg-config file is broken
214            prefix = self.get_pkgconfig_variable('exec_prefix', {})
215            if prefix:
216                self.bindir = os.path.join(prefix, 'bin')
217
218    @staticmethod
219    @abc.abstractmethod
220    def get_pkgconfig_host_bins(core: PkgConfigDependency) -> T.Optional[str]:
221        pass
222
223    @abc.abstractmethod
224    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
225        pass
226
227    def log_info(self) -> str:
228        return 'pkg-config'
229
230
231class QmakeQtDependency(_QtBase, ConfigToolDependency, metaclass=abc.ABCMeta):
232
233    """Find Qt using Qmake as a config-tool."""
234
235    tool_name = 'qmake'
236    version_arg = '-v'
237
238    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
239        _QtBase.__init__(self, name, kwargs)
240        self.tools = [f'qmake-{self.qtname}', 'qmake']
241
242        # Add additional constraints that the Qt version is met, but preserve
243        # any version requrements the user has set as well. For example, if Qt5
244        # is requested, add "">= 5, < 6", but if the user has ">= 5.6", don't
245        # lose that.
246        kwargs = kwargs.copy()
247        _vers = mesonlib.listify(kwargs.get('version', []))
248        _vers.extend([f'>= {self.qtver}', f'< {int(self.qtver) + 1}'])
249        kwargs['version'] = _vers
250
251        ConfigToolDependency.__init__(self, name, env, kwargs)
252        if not self.found():
253            return
254
255        # Query library path, header path, and binary path
256        stdo = self.get_config_value(['-query'], 'args')
257        qvars: T.Dict[str, str] = {}
258        for line in stdo:
259            line = line.strip()
260            if line == '':
261                continue
262            k, v = line.split(':', 1)
263            qvars[k] = v
264        # Qt on macOS uses a framework, but Qt for iOS/tvOS does not
265        xspec = qvars.get('QMAKE_XSPEC', '')
266        if self.env.machines.host.is_darwin() and not any(s in xspec for s in ['ios', 'tvos']):
267            mlog.debug("Building for macOS, looking for framework")
268            self._framework_detect(qvars, self.requested_modules, kwargs)
269            # Sometimes Qt is built not as a framework (for instance, when using conan pkg manager)
270            # skip and fall back to normal procedure then
271            if self.is_found:
272                return
273            else:
274                mlog.debug("Building for macOS, couldn't find framework, falling back to library search")
275        incdir = qvars['QT_INSTALL_HEADERS']
276        self.compile_args.append('-I' + incdir)
277        libdir = qvars['QT_INSTALL_LIBS']
278        # Used by qt.compilers_detect()
279        self.bindir = get_qmake_host_bins(qvars)
280
281        # Use the buildtype by default, but look at the b_vscrt option if the
282        # compiler supports it.
283        is_debug = self.env.coredata.get_option(mesonlib.OptionKey('buildtype')) == 'debug'
284        if mesonlib.OptionKey('b_vscrt') in self.env.coredata.options:
285            if self.env.coredata.options[mesonlib.OptionKey('b_vscrt')].value in {'mdd', 'mtd'}:
286                is_debug = True
287        modules_lib_suffix = _get_modules_lib_suffix(self.version, self.env.machines[self.for_machine], is_debug)
288
289        for module in self.requested_modules:
290            mincdir = os.path.join(incdir, 'Qt' + module)
291            self.compile_args.append('-I' + mincdir)
292
293            if module == 'QuickTest':
294                define_base = 'QMLTEST'
295            elif module == 'Test':
296                define_base = 'TESTLIB'
297            else:
298                define_base = module.upper()
299            self.compile_args.append(f'-DQT_{define_base}_LIB')
300
301            if self.private_headers:
302                priv_inc = self.get_private_includes(mincdir, module)
303                for directory in priv_inc:
304                    self.compile_args.append('-I' + directory)
305            libfiles = self.clib_compiler.find_library(
306                self.qtpkgname + module + modules_lib_suffix, self.env,
307                mesonlib.listify(libdir)) # TODO: shouldn't be necissary
308            if libfiles:
309                libfile = libfiles[0]
310            else:
311                mlog.log("Could not find:", module,
312                         self.qtpkgname + module + modules_lib_suffix,
313                         'in', libdir)
314                self.is_found = False
315                break
316            self.link_args.append(libfile)
317
318        if self.env.machines[self.for_machine].is_windows() and self.qtmain:
319            if not self._link_with_qtmain(is_debug, libdir):
320                self.is_found = False
321
322    def _sanitize_version(self, version: str) -> str:
323        m = re.search(rf'({self.qtver}(\.\d+)+)', version)
324        if m:
325            return m.group(0).rstrip('.')
326        return version
327
328    @abc.abstractmethod
329    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
330        pass
331
332    def _framework_detect(self, qvars: T.Dict[str, str], modules: T.List[str], kwargs: T.Dict[str, T.Any]) -> None:
333        libdir = qvars['QT_INSTALL_LIBS']
334
335        # ExtraFrameworkDependency doesn't support any methods
336        fw_kwargs = kwargs.copy()
337        fw_kwargs.pop('method', None)
338        fw_kwargs['paths'] = [libdir]
339
340        for m in modules:
341            fname = 'Qt' + m
342            mlog.debug('Looking for qt framework ' + fname)
343            fwdep = QtExtraFrameworkDependency(fname, self.env, fw_kwargs, language=self.language)
344            if fwdep.found():
345                self.compile_args.append('-F' + libdir)
346                self.compile_args += fwdep.get_compile_args(with_private_headers=self.private_headers,
347                                                            qt_version=self.version)
348                self.link_args += fwdep.get_link_args()
349            else:
350                self.is_found = False
351                break
352        else:
353            self.is_found = True
354            # Used by self.compilers_detect()
355            self.bindir = get_qmake_host_bins(qvars)
356
357    def log_info(self) -> str:
358        return 'qmake'
359
360
361class Qt4ConfigToolDependency(QmakeQtDependency):
362
363    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
364        return []
365
366
367class Qt5ConfigToolDependency(QmakeQtDependency):
368
369    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
370        return _qt_get_private_includes(mod_inc_dir, module, self.version)
371
372
373class Qt6ConfigToolDependency(QmakeQtDependency):
374
375    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
376        return _qt_get_private_includes(mod_inc_dir, module, self.version)
377
378
379class Qt4PkgConfigDependency(QtPkgConfigDependency):
380
381    @staticmethod
382    def get_pkgconfig_host_bins(core: PkgConfigDependency) -> T.Optional[str]:
383        # Only return one bins dir, because the tools are generally all in one
384        # directory for Qt4, in Qt5, they must all be in one directory. Return
385        # the first one found among the bin variables, in case one tool is not
386        # configured to be built.
387        applications = ['moc', 'uic', 'rcc', 'lupdate', 'lrelease']
388        for application in applications:
389            try:
390                return os.path.dirname(core.get_pkgconfig_variable('%s_location' % application, {}))
391            except mesonlib.MesonException:
392                pass
393        return None
394
395    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
396        return []
397
398
399class Qt5PkgConfigDependency(QtPkgConfigDependency):
400
401    @staticmethod
402    def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str:
403        return core.get_pkgconfig_variable('host_bins', {})
404
405    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
406        return _qt_get_private_includes(mod_inc_dir, module, self.version)
407
408
409class Qt6PkgConfigDependency(QtPkgConfigDependency):
410
411    @staticmethod
412    def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str:
413        return core.get_pkgconfig_variable('host_bins', {})
414
415    def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]:
416        return _qt_get_private_includes(mod_inc_dir, module, self.version)
417
418
419qt4_factory = DependencyFactory(
420    'qt4',
421    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
422    pkgconfig_class=Qt4PkgConfigDependency,
423    configtool_class=Qt4ConfigToolDependency,
424)
425
426qt5_factory = DependencyFactory(
427    'qt5',
428    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
429    pkgconfig_class=Qt5PkgConfigDependency,
430    configtool_class=Qt5ConfigToolDependency,
431)
432
433qt6_factory = DependencyFactory(
434    'qt6',
435    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
436    pkgconfig_class=Qt6PkgConfigDependency,
437    configtool_class=Qt6ConfigToolDependency,
438)
439