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