diff options
Diffstat (limited to 'mesonbuild/dependencies/qt.py')
-rw-r--r-- | mesonbuild/dependencies/qt.py | 297 |
1 files changed, 157 insertions, 140 deletions
diff --git a/mesonbuild/dependencies/qt.py b/mesonbuild/dependencies/qt.py index 97a8a7f..1059871 100644 --- a/mesonbuild/dependencies/qt.py +++ b/mesonbuild/dependencies/qt.py @@ -17,25 +17,22 @@ """Dependency finders for the Qt framework.""" import abc -import collections import re import os import typing as T from . import ( - ExtraFrameworkDependency, ExternalDependency, DependencyException, DependencyMethods, - PkgConfigDependency + ExtraFrameworkDependency, DependencyException, DependencyMethods, + PkgConfigDependency, ) -from .base import ConfigToolDependency +from .base import ConfigToolDependency, DependencyFactory from .. import mlog from .. import mesonlib -from ..programs import find_external_program if T.TYPE_CHECKING: from ..compilers import Compiler from ..envconfig import MachineInfo from ..environment import Environment - from ..programs import ExternalProgram def _qt_get_private_includes(mod_inc_dir: str, module: str, mod_version: str) -> T.List[str]: @@ -114,70 +111,78 @@ class QtExtraFrameworkDependency(ExtraFrameworkDependency): return [] -class QtBaseDependency(ExternalDependency, metaclass=abc.ABCMeta): +class _QtBase: - version: T.Optional[str] + """Mixin class for shared componenets between PkgConfig and Qmake.""" - def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]): - super().__init__(name, env, kwargs, language='cpp') + link_args: T.List[str] + clib_compiler: 'Compiler' + env: 'Environment' + + def __init__(self, name: str, kwargs: T.Dict[str, T.Any]): self.qtname = name.capitalize() self.qtver = name[-1] if self.qtver == "4": self.qtpkgname = 'Qt' else: self.qtpkgname = self.qtname - self.root = '/usr' - self.bindir: T.Optional[str] = None + self.private_headers = T.cast(bool, kwargs.get('private_headers', False)) - mods = mesonlib.stringlistify(mesonlib.extract_as_list(kwargs, 'modules')) - self.requested_modules = mods - if not mods: + + self.requested_modules = mesonlib.stringlistify(mesonlib.extract_as_list(kwargs, 'modules')) + if not self.requested_modules: raise DependencyException('No ' + self.qtname + ' modules specified.') - self.from_text = 'pkg-config' self.qtmain = T.cast(bool, kwargs.get('main', False)) if not isinstance(self.qtmain, bool): raise DependencyException('"main" argument must be a boolean') - # Keep track of the detection methods used, for logging purposes. - methods: T.List[str] = [] - # Prefer pkg-config, then fallback to `qmake -query` - if DependencyMethods.PKGCONFIG in self.methods: - mlog.debug('Trying to find qt with pkg-config') - self._pkgconfig_detect(mods, kwargs) - methods.append('pkgconfig') - if not self.is_found and DependencyMethods.QMAKE in self.methods: - mlog.debug('Trying to find qt with qmake') - self.from_text = self._qmake_detect(mods, kwargs) - methods.append('qmake-' + self.name) - methods.append('qmake') - if not self.is_found: - # Reset compile args and link args + def _link_with_qtmain(self, is_debug: bool, libdir: T.Union[str, T.List[str]]) -> bool: + libdir = mesonlib.listify(libdir) # TODO: shouldn't be necessary + base_name = 'qtmaind' if is_debug else 'qtmain' + qtmain = self.clib_compiler.find_library(base_name, self.env, libdir) + if qtmain: + self.link_args.append(qtmain[0]) + return True + return False + + 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 = [] - self.from_text = mlog.format_list(methods) - self.version = None - - @abc.abstractmethod - def get_pkgconfig_host_bins(self, core: PkgConfigDependency) -> T.Optional[str]: - pass - def _pkgconfig_detect(self, mods: T.List[str], kwargs: T.Dict[str, T.Any]) -> None: - # We set the value of required to False so that we can try the - # qmake-based fallback if pkg-config fails. - kwargs['required'] = False - modules: T.MutableMapping[str, PkgConfigDependency] = collections.OrderedDict() - for module in mods: - modules[module] = PkgConfigDependency(self.qtpkgname + module, self.env, - kwargs, language=self.language) - for m_name, m in modules.items(): - if not m.found(): + 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 - self.compile_args += m.get_compile_args() if self.private_headers: - qt_inc_dir = m.get_pkgconfig_variable('includedir', dict()) - mod_private_dir = os.path.join(qt_inc_dir, 'Qt' + m_name) + qt_inc_dir = mod.get_pkgconfig_variable('includedir', {}) + 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 @@ -185,94 +190,93 @@ class QtBaseDependency(ExternalDependency, metaclass=abc.ABCMeta): # 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_name, m.version) + mod_private_inc = _qt_get_private_includes(mod_private_dir, m, mod.version) for directory in mod_private_inc: - self.compile_args.append('-I' + directory) - self.link_args += m.get_link_args() - - if 'Core' in modules: - core = modules['Core'] - else: - corekwargs = {'required': 'false', 'silent': 'true'} - core = PkgConfigDependency(self.qtpkgname + 'Core', self.env, corekwargs, - language=self.language) - modules['Core'] = core + 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 core.get_link_args(): - if arg == '-l%s' % debug_lib_name or arg.endswith('%s.lib' % debug_lib_name) or arg.endswith('%s.a' % debug_lib_name): + 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 = core.get_pkgconfig_variable('libdir', {}) + libdir = self.get_pkgconfig_variable('libdir', {}) if not self._link_with_qtmain(is_debug, libdir): self.is_found = False return - self.is_found = True - self.version = m.version - self.pcdep = list(modules.values()) - # Try to detect moc, uic, rcc - # Used by self.compilers_detect() - self.bindir = self.get_pkgconfig_host_bins(core) + 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 = core.get_pkgconfig_variable('exec_prefix', {}) + prefix = self.get_pkgconfig_variable('exec_prefix', {}) if prefix: self.bindir = os.path.join(prefix, 'bin') - def search_qmake(self) -> T.Generator['ExternalProgram', None, None]: - for qmake in ('qmake-' + self.name, 'qmake'): - yield from find_external_program(self.env, self.for_machine, qmake, 'QMake', [qmake]) + @staticmethod + @abc.abstractmethod + def get_pkgconfig_host_bins(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.qtname}', 'qmake'] + + # Add additional constraits that the Qt version is met, but preserve + # any version requrements the user has set as well. For exmaple, 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 - def _qmake_detect(self, mods: T.List[str], kwargs: T.Dict[str, T.Any]) -> T.Optional[str]: - for qmake in self.search_qmake(): - if not qmake.found(): - continue - # Check that the qmake is for qt5 - pc, stdo = mesonlib.Popen_safe(qmake.get_command() + ['-v'])[0:2] - if pc.returncode != 0: - continue - if not 'Qt version ' + self.qtver in stdo: - mlog.log('QMake is not for ' + self.qtname) - continue - # Found qmake for Qt5! - self.qmake = qmake - break - else: - # Didn't find qmake :( - self.is_found = False - return None - self.version = re.search(self.qtver + r'(\.\d+)+', stdo).group(0) # Query library path, header path, and binary path - mlog.log("Found qmake:", mlog.bold(self.qmake.get_path()), '(%s)' % self.version) - stdo = mesonlib.Popen_safe(self.qmake.get_command() + ['-query'])[1] - qvars = {} - for line in stdo.split('\n'): + stdo = self.get_config_value(['-query'], 'args') + qvars: T.Dict[str, str] = {} + for line in stdo: line = line.strip() if line == '': continue - (k, v) = tuple(line.split(':', 1)) + 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, mods, kwargs) + 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 self.qmake.name + 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 self.compilers_detect() + # Used by qt.compilers_detect() self.bindir = get_qmake_host_bins(qvars) - self.is_found = True # Use the buildtype by default, but look at the b_vscrt option if the # compiler supports it. @@ -282,7 +286,7 @@ class QtBaseDependency(ExternalDependency, metaclass=abc.ABCMeta): is_debug = True modules_lib_suffix = _get_modules_lib_suffix(self.version, self.env.machines[self.for_machine], is_debug) - for module in mods: + for module in self.requested_modules: mincdir = os.path.join(incdir, 'Qt' + module) self.compile_args.append('-I' + mincdir) @@ -292,7 +296,7 @@ class QtBaseDependency(ExternalDependency, metaclass=abc.ABCMeta): define_base = 'TESTLIB' else: define_base = module.upper() - self.compile_args.append('-DQT_%s_LIB' % define_base) + self.compile_args.append(f'-DQT_{define_base}_LIB') if self.private_headers: priv_inc = self.get_private_includes(mincdir, module) @@ -315,16 +319,15 @@ class QtBaseDependency(ExternalDependency, metaclass=abc.ABCMeta): if not self._link_with_qtmain(is_debug, libdir): self.is_found = False - return self.qmake.name + def _sanitize_version(self, version: str) -> str: + m = re.search(rf'({self.qtver}(\.\d+)+)', version) + if m: + return m.group(0).rstrip('.') + return version - def _link_with_qtmain(self, is_debug: bool, libdir: T.Union[str, T.List[str]]) -> bool: - libdir = mesonlib.listify(libdir) # TODO: shouldn't be necessary - base_name = 'qtmaind' if is_debug else 'qtmain' - qtmain = self.clib_compiler.find_library(base_name, self.env, libdir) - if qtmain: - self.link_args.append(qtmain[0]) - return True - return False + @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'] @@ -344,43 +347,36 @@ class QtBaseDependency(ExternalDependency, metaclass=abc.ABCMeta): 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) - @staticmethod - def get_methods() -> T.List[DependencyMethods]: - return [DependencyMethods.PKGCONFIG, DependencyMethods.QMAKE] + def log_info(self) -> str: + return 'qmake' - @staticmethod - def get_exe_args(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() + +class Qt4ConfigToolDependency(QmakeQtDependency): def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: return [] - def log_details(self) -> str: - module_str = ', '.join(self.requested_modules) - return 'modules: ' + module_str - def log_info(self) -> str: - return f'{self.from_text}' +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(QmakeQtDependency): - def log_tried(self) -> str: - return self.from_text + 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 Qt4Dependency(QtBaseDependency): - def __init__(self, env: 'Environment', kwargs: T.Dict[str, T.Any]): - QtBaseDependency.__init__(self, 'qt4', env, kwargs) +class Qt4PkgConfigDependency(QtPkgConfigDependency): @staticmethod def get_pkgconfig_host_bins(core: PkgConfigDependency) -> T.Optional[str]: @@ -396,10 +392,11 @@ class Qt4Dependency(QtBaseDependency): pass return None + def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: + return [] + -class Qt5Dependency(QtBaseDependency): - def __init__(self, env: 'Environment', kwargs: T.Dict[str, T.Any]): - QtBaseDependency.__init__(self, 'qt5', env, kwargs) +class Qt5PkgConfigDependency(QtPkgConfigDependency): @staticmethod def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str: @@ -409,9 +406,7 @@ class Qt5Dependency(QtBaseDependency): return _qt_get_private_includes(mod_inc_dir, module, self.version) -class Qt6Dependency(QtBaseDependency): - def __init__(self, env: 'Environment', kwargs: T.Dict[str, T.Any]): - QtBaseDependency.__init__(self, 'qt6', env, kwargs) +class Qt6PkgConfigDependency(QtPkgConfigDependency): @staticmethod def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str: @@ -419,3 +414,25 @@ class Qt6Dependency(QtBaseDependency): 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, +) |