diff options
Diffstat (limited to '')
-rw-r--r-- | mesonbuild/dependencies/qt.py | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/mesonbuild/dependencies/qt.py b/mesonbuild/dependencies/qt.py new file mode 100644 index 0000000..6dd712d --- /dev/null +++ b/mesonbuild/dependencies/qt.py @@ -0,0 +1,486 @@ +# Copyright 2013-2017 The Meson development team +# Copyright © 2021 Intel Corporation +# SPDX-license-identifier: Apache-2.0 + +# 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 + +"""Dependency finders for the Qt framework.""" + +import abc +import re +import os +import typing as T + +from .base import DependencyException, DependencyMethods +from .configtool import ConfigToolDependency +from .framework import ExtraFrameworkDependency +from .pkgconfig import PkgConfigDependency +from .factory import DependencyFactory +from .. import mlog +from .. import mesonlib + +if T.TYPE_CHECKING: + from ..compilers import Compiler + from ..envconfig import MachineInfo + from ..environment import Environment + from ..dependencies import MissingCompiler + + +def _qt_get_private_includes(mod_inc_dir: str, module: str, mod_version: str) -> T.List[str]: + # usually Qt5 puts private headers in /QT_INSTALL_HEADERS/module/VERSION/module/private + # except for at least QtWebkit and Enginio where the module version doesn't match Qt version + # as an example with Qt 5.10.1 on linux you would get: + # /usr/include/qt5/QtCore/5.10.1/QtCore/private/ + # /usr/include/qt5/QtWidgets/5.10.1/QtWidgets/private/ + # /usr/include/qt5/QtWebKit/5.212.0/QtWebKit/private/ + + # on Qt4 when available private folder is directly in module folder + # like /usr/include/QtCore/private/ + if int(mod_version.split('.')[0]) < 5: + return [] + + private_dir = os.path.join(mod_inc_dir, mod_version) + # fallback, let's try to find a directory with the latest version + if not os.path.exists(private_dir): + dirs = [filename for filename in os.listdir(mod_inc_dir) + if os.path.isdir(os.path.join(mod_inc_dir, filename))] + + for dirname in sorted(dirs, reverse=True): + if len(dirname.split('.')) == 3: + private_dir = dirname + break + return [private_dir, os.path.join(private_dir, 'Qt' + module)] + + +def get_qmake_host_bins(qvars: T.Dict[str, str]) -> str: + # Prefer QT_HOST_BINS (qt5, correct for cross and native compiling) + # but fall back to QT_INSTALL_BINS (qt4) + if 'QT_HOST_BINS' in qvars: + return qvars['QT_HOST_BINS'] + return qvars['QT_INSTALL_BINS'] + + +def get_qmake_host_libexecs(qvars: T.Dict[str, str]) -> T.Optional[str]: + if 'QT_HOST_LIBEXECS' in qvars: + return qvars['QT_HOST_LIBEXECS'] + return qvars.get('QT_INSTALL_LIBEXECS') + + +def _get_modules_lib_suffix(version: str, info: 'MachineInfo', is_debug: bool) -> str: + """Get the module suffix based on platform and debug type.""" + suffix = '' + if info.is_windows(): + if is_debug: + suffix += 'd' + if version.startswith('4'): + suffix += '4' + if info.is_darwin(): + if is_debug: + suffix += '_debug' + if mesonlib.version_compare(version, '>= 5.14.0'): + if info.is_android(): + if info.cpu_family == 'x86': + suffix += '_x86' + elif info.cpu_family == 'x86_64': + suffix += '_x86_64' + elif info.cpu_family == 'arm': + suffix += '_armeabi-v7a' + elif info.cpu_family == 'aarch64': + suffix += '_arm64-v8a' + else: + mlog.warning(f'Android target arch "{info.cpu_family}"" for Qt5 is unknown, ' + 'module detection may not work') + return suffix + + +class QtExtraFrameworkDependency(ExtraFrameworkDependency): + def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None): + super().__init__(name, env, kwargs, language=language) + self.mod_name = name[2:] + + def get_compile_args(self, with_private_headers: bool = False, qt_version: str = "0") -> T.List[str]: + if self.found(): + mod_inc_dir = os.path.join(self.framework_path, 'Headers') + args = ['-I' + mod_inc_dir] + if with_private_headers: + args += ['-I' + dirname for dirname in _qt_get_private_includes(mod_inc_dir, self.mod_name, qt_version)] + return args + return [] + + +class _QtBase: + + """Mixin class for shared components between PkgConfig and Qmake.""" + + link_args: T.List[str] + clib_compiler: T.Union['MissingCompiler', 'Compiler'] + env: 'Environment' + libexecdir: T.Optional[str] = None + + def __init__(self, name: str, kwargs: T.Dict[str, T.Any]): + self.name = name + self.qtname = name.capitalize() + self.qtver = name[-1] + if self.qtver == "4": + self.qtpkgname = 'Qt' + else: + self.qtpkgname = self.qtname + + self.private_headers = T.cast('bool', kwargs.get('private_headers', False)) + + self.requested_modules = mesonlib.stringlistify(mesonlib.extract_as_list(kwargs, 'modules')) + if not self.requested_modules: + raise DependencyException('No ' + self.qtname + ' modules specified.') + + self.qtmain = T.cast('bool', kwargs.get('main', False)) + if not isinstance(self.qtmain, bool): + raise DependencyException('"main" argument must be a boolean') + + def _link_with_qt_winmain(self, is_debug: bool, libdir: T.Union[str, T.List[str]]) -> bool: + libdir = mesonlib.listify(libdir) # TODO: shouldn't be necessary + base_name = self.get_qt_winmain_base_name(is_debug) + qt_winmain = self.clib_compiler.find_library(base_name, self.env, libdir) + if qt_winmain: + self.link_args.append(qt_winmain[0]) + return True + return False + + def get_qt_winmain_base_name(self, is_debug: bool) -> str: + return 'qtmaind' if is_debug else 'qtmain' + + def get_exe_args(self, compiler: 'Compiler') -> T.List[str]: + # Originally this was -fPIE but nowadays the default + # for upstream and distros seems to be -reduce-relocations + # which requires -fPIC. This may cause a performance + # penalty when using self-built Qt or on platforms + # where -fPIC is not required. If this is an issue + # for you, patches are welcome. + return compiler.get_pic_args() + + def log_details(self) -> str: + return f'modules: {", ".join(sorted(self.requested_modules))}' + + +class QtPkgConfigDependency(_QtBase, PkgConfigDependency, metaclass=abc.ABCMeta): + + """Specialization of the PkgConfigDependency for Qt.""" + + def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]): + _QtBase.__init__(self, name, kwargs) + + # Always use QtCore as the "main" dependency, since it has the extra + # pkg-config variables that a user would expect to get. If "Core" is + # not a requested module, delete the compile and link arguments to + # avoid linking with something they didn't ask for + PkgConfigDependency.__init__(self, self.qtpkgname + 'Core', env, kwargs) + if 'Core' not in self.requested_modules: + self.compile_args = [] + self.link_args = [] + + for m in self.requested_modules: + mod = PkgConfigDependency(self.qtpkgname + m, self.env, kwargs, language=self.language) + if not mod.found(): + self.is_found = False + return + if self.private_headers: + qt_inc_dir = mod.get_pkgconfig_variable('includedir', [], None) + mod_private_dir = os.path.join(qt_inc_dir, 'Qt' + m) + if not os.path.isdir(mod_private_dir): + # At least some versions of homebrew don't seem to set this + # up correctly. /usr/local/opt/qt/include/Qt + m_name is a + # symlink to /usr/local/opt/qt/include, but the pkg-config + # file points to /usr/local/Cellar/qt/x.y.z/Headers/, and + # the Qt + m_name there is not a symlink, it's a file + mod_private_dir = qt_inc_dir + mod_private_inc = _qt_get_private_includes(mod_private_dir, m, mod.version) + for directory in mod_private_inc: + mod.compile_args.append('-I' + directory) + self._add_sub_dependency([lambda: mod]) + + if self.env.machines[self.for_machine].is_windows() and self.qtmain: + # Check if we link with debug binaries + debug_lib_name = self.qtpkgname + 'Core' + _get_modules_lib_suffix(self.version, self.env.machines[self.for_machine], True) + is_debug = False + for arg in self.get_link_args(): + if arg == f'-l{debug_lib_name}' or arg.endswith(f'{debug_lib_name}.lib') or arg.endswith(f'{debug_lib_name}.a'): + is_debug = True + break + libdir = self.get_pkgconfig_variable('libdir', [], None) + if not self._link_with_qt_winmain(is_debug, libdir): + self.is_found = False + return + + self.bindir = self.get_pkgconfig_host_bins(self) + if not self.bindir: + # If exec_prefix is not defined, the pkg-config file is broken + prefix = self.get_pkgconfig_variable('exec_prefix', [], None) + if prefix: + self.bindir = os.path.join(prefix, 'bin') + + self.libexecdir = self.get_pkgconfig_host_libexecs(self) + + @staticmethod + @abc.abstractmethod + def get_pkgconfig_host_bins(core: PkgConfigDependency) -> T.Optional[str]: + pass + + @staticmethod + @abc.abstractmethod + def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> T.Optional[str]: + pass + + @abc.abstractmethod + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + pass + + def log_info(self) -> str: + return 'pkg-config' + + +class QmakeQtDependency(_QtBase, ConfigToolDependency, metaclass=abc.ABCMeta): + + """Find Qt using Qmake as a config-tool.""" + + tool_name = 'qmake' + version_arg = '-v' + + def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]): + _QtBase.__init__(self, name, kwargs) + self.tools = [f'qmake{self.qtver}', f'qmake-{self.name}', 'qmake'] + + # Add additional constraints that the Qt version is met, but preserve + # any version requrements the user has set as well. For example, if Qt5 + # is requested, add "">= 5, < 6", but if the user has ">= 5.6", don't + # lose that. + kwargs = kwargs.copy() + _vers = mesonlib.listify(kwargs.get('version', [])) + _vers.extend([f'>= {self.qtver}', f'< {int(self.qtver) + 1}']) + kwargs['version'] = _vers + + ConfigToolDependency.__init__(self, name, env, kwargs) + if not self.found(): + return + + # Query library path, header path, and binary path + stdo = self.get_config_value(['-query'], 'args') + qvars: T.Dict[str, str] = {} + for line in stdo: + line = line.strip() + if line == '': + continue + k, v = line.split(':', 1) + qvars[k] = v + # Qt on macOS uses a framework, but Qt for iOS/tvOS does not + xspec = qvars.get('QMAKE_XSPEC', '') + if self.env.machines.host.is_darwin() and not any(s in xspec for s in ['ios', 'tvos']): + mlog.debug("Building for macOS, looking for framework") + self._framework_detect(qvars, self.requested_modules, kwargs) + # Sometimes Qt is built not as a framework (for instance, when using conan pkg manager) + # skip and fall back to normal procedure then + if self.is_found: + return + else: + mlog.debug("Building for macOS, couldn't find framework, falling back to library search") + incdir = qvars['QT_INSTALL_HEADERS'] + self.compile_args.append('-I' + incdir) + libdir = qvars['QT_INSTALL_LIBS'] + # Used by qt.compilers_detect() + self.bindir = get_qmake_host_bins(qvars) + self.libexecdir = get_qmake_host_libexecs(qvars) + + # Use the buildtype by default, but look at the b_vscrt option if the + # compiler supports it. + is_debug = self.env.coredata.get_option(mesonlib.OptionKey('buildtype')) == 'debug' + if mesonlib.OptionKey('b_vscrt') in self.env.coredata.options: + if self.env.coredata.options[mesonlib.OptionKey('b_vscrt')].value in {'mdd', 'mtd'}: + is_debug = True + modules_lib_suffix = _get_modules_lib_suffix(self.version, self.env.machines[self.for_machine], is_debug) + + for module in self.requested_modules: + mincdir = os.path.join(incdir, 'Qt' + module) + self.compile_args.append('-I' + mincdir) + + if module == 'QuickTest': + define_base = 'QMLTEST' + elif module == 'Test': + define_base = 'TESTLIB' + else: + define_base = module.upper() + self.compile_args.append(f'-DQT_{define_base}_LIB') + + if self.private_headers: + priv_inc = self.get_private_includes(mincdir, module) + for directory in priv_inc: + self.compile_args.append('-I' + directory) + libfiles = self.clib_compiler.find_library( + self.qtpkgname + module + modules_lib_suffix, self.env, + mesonlib.listify(libdir)) # TODO: shouldn't be necissary + if libfiles: + libfile = libfiles[0] + else: + mlog.log("Could not find:", module, + self.qtpkgname + module + modules_lib_suffix, + 'in', libdir) + self.is_found = False + break + self.link_args.append(libfile) + + if self.env.machines[self.for_machine].is_windows() and self.qtmain: + if not self._link_with_qt_winmain(is_debug, libdir): + self.is_found = False + + def _sanitize_version(self, version: str) -> str: + m = re.search(rf'({self.qtver}(\.\d+)+)', version) + if m: + return m.group(0).rstrip('.') + return version + + @abc.abstractmethod + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + pass + + def _framework_detect(self, qvars: T.Dict[str, str], modules: T.List[str], kwargs: T.Dict[str, T.Any]) -> None: + libdir = qvars['QT_INSTALL_LIBS'] + + # ExtraFrameworkDependency doesn't support any methods + fw_kwargs = kwargs.copy() + fw_kwargs.pop('method', None) + fw_kwargs['paths'] = [libdir] + + for m in modules: + fname = 'Qt' + m + mlog.debug('Looking for qt framework ' + fname) + fwdep = QtExtraFrameworkDependency(fname, self.env, fw_kwargs, language=self.language) + if fwdep.found(): + self.compile_args.append('-F' + libdir) + self.compile_args += fwdep.get_compile_args(with_private_headers=self.private_headers, + qt_version=self.version) + self.link_args += fwdep.get_link_args() + else: + self.is_found = False + break + else: + self.is_found = True + # Used by self.compilers_detect() + self.bindir = get_qmake_host_bins(qvars) + self.libexecdir = get_qmake_host_libexecs(qvars) + + def log_info(self) -> str: + return 'qmake' + + +class Qt6WinMainMixin: + + def get_qt_winmain_base_name(self, is_debug: bool) -> str: + return 'Qt6EntryPointd' if is_debug else 'Qt6EntryPoint' + + +class Qt4ConfigToolDependency(QmakeQtDependency): + + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + return [] + + +class Qt5ConfigToolDependency(QmakeQtDependency): + + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + return _qt_get_private_includes(mod_inc_dir, module, self.version) + + +class Qt6ConfigToolDependency(Qt6WinMainMixin, QmakeQtDependency): + + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + return _qt_get_private_includes(mod_inc_dir, module, self.version) + + +class Qt4PkgConfigDependency(QtPkgConfigDependency): + + @staticmethod + def get_pkgconfig_host_bins(core: PkgConfigDependency) -> T.Optional[str]: + # Only return one bins dir, because the tools are generally all in one + # directory for Qt4, in Qt5, they must all be in one directory. Return + # the first one found among the bin variables, in case one tool is not + # configured to be built. + applications = ['moc', 'uic', 'rcc', 'lupdate', 'lrelease'] + for application in applications: + try: + return os.path.dirname(core.get_pkgconfig_variable(f'{application}_location', [], None)) + except mesonlib.MesonException: + pass + return None + + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + return [] + + @staticmethod + def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str: + return None + + +class Qt5PkgConfigDependency(QtPkgConfigDependency): + + @staticmethod + def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str: + return core.get_pkgconfig_variable('host_bins', [], None) + + @staticmethod + def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str: + return None + + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + return _qt_get_private_includes(mod_inc_dir, module, self.version) + + +class Qt6PkgConfigDependency(Qt6WinMainMixin, QtPkgConfigDependency): + + def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]): + super().__init__(name, env, kwargs) + if not self.libexecdir: + mlog.debug(f'detected Qt6 {self.version} pkg-config dependency does not ' + 'have proper tools support, ignoring') + self.is_found = False + + @staticmethod + def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str: + return core.get_pkgconfig_variable('bindir', [], None) + + @staticmethod + def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str: + # Qt6 pkg-config for Qt defines libexecdir from 6.3+ + return core.get_pkgconfig_variable('libexecdir', [], None) + + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + return _qt_get_private_includes(mod_inc_dir, module, self.version) + + +qt4_factory = DependencyFactory( + 'qt4', + [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], + pkgconfig_class=Qt4PkgConfigDependency, + configtool_class=Qt4ConfigToolDependency, +) + +qt5_factory = DependencyFactory( + 'qt5', + [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], + pkgconfig_class=Qt5PkgConfigDependency, + configtool_class=Qt5ConfigToolDependency, +) + +qt6_factory = DependencyFactory( + 'qt6', + [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], + pkgconfig_class=Qt6PkgConfigDependency, + configtool_class=Qt6ConfigToolDependency, +) |