ui.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. # Copyright 2013-2017 The Meson development team
  2. # Licensed under the Apache License, Version 2.0 (the "License");
  3. # you may not use this file except in compliance with the License.
  4. # You may obtain a copy of the License at
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. # Unless required by applicable law or agreed to in writing, software
  7. # distributed under the License is distributed on an "AS IS" BASIS,
  8. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. # See the License for the specific language governing permissions and
  10. # limitations under the License.
  11. # This file contains the detection logic for external dependencies that
  12. # are UI-related.
  13. import os
  14. import re
  15. import shutil
  16. import subprocess
  17. from collections import OrderedDict
  18. from .. import mlog
  19. from .. import mesonlib
  20. from ..mesonlib import (
  21. MesonException, Popen_safe, extract_as_list, for_windows,
  22. version_compare_many
  23. )
  24. from ..environment import detect_cpu
  25. from .base import DependencyException, DependencyMethods
  26. from .base import ExternalDependency, ExternalProgram
  27. from .base import ExtraFrameworkDependency, PkgConfigDependency
  28. from .base import ConfigToolDependency
  29. class GLDependency(ExternalDependency):
  30. def __init__(self, environment, kwargs):
  31. super().__init__('gl', environment, None, kwargs)
  32. if DependencyMethods.PKGCONFIG in self.methods:
  33. try:
  34. pcdep = PkgConfigDependency('gl', environment, kwargs)
  35. if pcdep.found():
  36. self.type_name = 'pkgconfig'
  37. self.is_found = True
  38. self.compile_args = pcdep.get_compile_args()
  39. self.link_args = pcdep.get_link_args()
  40. self.version = pcdep.get_version()
  41. return
  42. except Exception:
  43. pass
  44. if DependencyMethods.SYSTEM in self.methods:
  45. if mesonlib.is_osx():
  46. self.is_found = True
  47. # FIXME: Use AppleFrameworks dependency
  48. self.link_args = ['-framework', 'OpenGL']
  49. # FIXME: Detect version using self.compiler
  50. self.version = '1'
  51. return
  52. if mesonlib.is_windows():
  53. self.is_found = True
  54. # FIXME: Use self.compiler.find_library()
  55. self.link_args = ['-lopengl32']
  56. # FIXME: Detect version using self.compiler
  57. self.version = '1'
  58. return
  59. def get_methods(self):
  60. if mesonlib.is_osx() or mesonlib.is_windows():
  61. return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]
  62. else:
  63. return [DependencyMethods.PKGCONFIG]
  64. class GnuStepDependency(ConfigToolDependency):
  65. tools = ['gnustep-config']
  66. tool_name = 'gnustep-config'
  67. def __init__(self, environment, kwargs):
  68. super().__init__('gnustep', environment, 'objc', kwargs)
  69. if not self.is_found:
  70. return
  71. self.modules = kwargs.get('modules', [])
  72. self.compile_args = self.filter_args(
  73. self.get_config_value(['--objc-flags'], 'compile_args'))
  74. self.link_args = self.weird_filter(self.get_config_value(
  75. ['--gui-libs' if 'gui' in self.modules else '--base-libs'],
  76. 'link_args'))
  77. def find_config(self, versions=None):
  78. tool = self.tools[0]
  79. try:
  80. p, out = Popen_safe([tool, '--help'])[:2]
  81. except (FileNotFoundError, PermissionError):
  82. return (None, None)
  83. if p.returncode != 0:
  84. return (None, None)
  85. self.config = tool
  86. found_version = self.detect_version()
  87. if versions and not version_compare_many(found_version, versions)[0]:
  88. return (None, found_version)
  89. return (tool, found_version)
  90. def weird_filter(self, elems):
  91. """When building packages, the output of the enclosing Make is
  92. sometimes mixed among the subprocess output. I have no idea why. As a
  93. hack filter out everything that is not a flag.
  94. """
  95. return [e for e in elems if e.startswith('-')]
  96. def filter_args(self, args):
  97. """gnustep-config returns a bunch of garbage args such as -O2 and so
  98. on. Drop everything that is not needed.
  99. """
  100. result = []
  101. for f in args:
  102. if f.startswith('-D') \
  103. or f.startswith('-f') \
  104. or f.startswith('-I') \
  105. or f == '-pthread' \
  106. or (f.startswith('-W') and not f == '-Wall'):
  107. result.append(f)
  108. return result
  109. def detect_version(self):
  110. gmake = self.get_config_value(['--variable=GNUMAKE'], 'variable')[0]
  111. makefile_dir = self.get_config_value(['--variable=GNUSTEP_MAKEFILES'], 'variable')[0]
  112. # This Makefile has the GNUStep version set
  113. base_make = os.path.join(makefile_dir, 'Additional', 'base.make')
  114. # Print the Makefile variable passed as the argument. For instance, if
  115. # you run the make target `print-SOME_VARIABLE`, this will print the
  116. # value of the variable `SOME_VARIABLE`.
  117. printver = "print-%:\n\t@echo '$($*)'"
  118. env = os.environ.copy()
  119. # See base.make to understand why this is set
  120. env['FOUNDATION_LIB'] = 'gnu'
  121. p, o, e = Popen_safe([gmake, '-f', '-', '-f', base_make,
  122. 'print-GNUSTEP_BASE_VERSION'],
  123. env=env, write=printver, stdin=subprocess.PIPE)
  124. version = o.strip()
  125. if not version:
  126. mlog.debug("Couldn't detect GNUStep version, falling back to '1'")
  127. # Fallback to setting some 1.x version
  128. version = '1'
  129. return version
  130. class QtBaseDependency(ExternalDependency):
  131. def __init__(self, name, env, kwargs):
  132. super().__init__(name, env, 'cpp', kwargs)
  133. self.qtname = name.capitalize()
  134. self.qtver = name[-1]
  135. if self.qtver == "4":
  136. self.qtpkgname = 'Qt'
  137. else:
  138. self.qtpkgname = self.qtname
  139. self.root = '/usr'
  140. self.bindir = None
  141. mods = kwargs.get('modules', [])
  142. if isinstance(mods, str):
  143. mods = [mods]
  144. if not mods:
  145. raise DependencyException('No ' + self.qtname + ' modules specified.')
  146. type_text = 'cross' if env.is_cross_build() else 'native'
  147. found_msg = '{} {} {{}} dependency (modules: {}) found:' \
  148. ''.format(self.qtname, type_text, ', '.join(mods))
  149. from_text = 'pkg-config'
  150. # Keep track of the detection methods used, for logging purposes.
  151. methods = []
  152. # Prefer pkg-config, then fallback to `qmake -query`
  153. if DependencyMethods.PKGCONFIG in self.methods:
  154. self._pkgconfig_detect(mods, kwargs)
  155. methods.append('pkgconfig')
  156. if not self.is_found and DependencyMethods.QMAKE in self.methods:
  157. from_text = self._qmake_detect(mods, kwargs)
  158. methods.append('qmake-' + self.name)
  159. methods.append('qmake')
  160. if not self.is_found:
  161. # Reset compile args and link args
  162. self.compile_args = []
  163. self.link_args = []
  164. from_text = '(checked {})'.format(mlog.format_list(methods))
  165. self.version = 'none'
  166. if self.required:
  167. err_msg = '{} {} dependency not found {}' \
  168. ''.format(self.qtname, type_text, from_text)
  169. raise DependencyException(err_msg)
  170. if not self.silent:
  171. mlog.log(found_msg.format(from_text), mlog.red('NO'))
  172. return
  173. from_text = '`{}`'.format(from_text)
  174. if not self.silent:
  175. mlog.log(found_msg.format(from_text), mlog.green('YES'))
  176. def compilers_detect(self):
  177. "Detect Qt (4 or 5) moc, uic, rcc in the specified bindir or in PATH"
  178. if self.bindir:
  179. moc = ExternalProgram(os.path.join(self.bindir, 'moc'), silent=True)
  180. uic = ExternalProgram(os.path.join(self.bindir, 'uic'), silent=True)
  181. rcc = ExternalProgram(os.path.join(self.bindir, 'rcc'), silent=True)
  182. lrelease = ExternalProgram(os.path.join(self.bindir, 'lrelease'), silent=True)
  183. else:
  184. # We don't accept unsuffixed 'moc', 'uic', and 'rcc' because they
  185. # are sometimes older, or newer versions.
  186. moc = ExternalProgram('moc-' + self.name, silent=True)
  187. uic = ExternalProgram('uic-' + self.name, silent=True)
  188. rcc = ExternalProgram('rcc-' + self.name, silent=True)
  189. lrelease = ExternalProgram('lrelease-' + self.name, silent=True)
  190. return moc, uic, rcc, lrelease
  191. def _pkgconfig_detect(self, mods, kwargs):
  192. # We set the value of required to False so that we can try the
  193. # qmake-based fallback if pkg-config fails.
  194. kwargs['required'] = False
  195. modules = OrderedDict()
  196. for module in mods:
  197. modules[module] = PkgConfigDependency(self.qtpkgname + module, self.env,
  198. kwargs, language=self.language)
  199. for m in modules.values():
  200. if not m.found():
  201. self.is_found = False
  202. return
  203. self.compile_args += m.get_compile_args()
  204. self.link_args += m.get_link_args()
  205. self.is_found = True
  206. self.version = m.version
  207. # Try to detect moc, uic, rcc
  208. if 'Core' in modules:
  209. core = modules['Core']
  210. else:
  211. corekwargs = {'required': 'false', 'silent': 'true'}
  212. core = PkgConfigDependency(self.qtpkgname + 'Core', self.env, corekwargs,
  213. language=self.language)
  214. # Used by self.compilers_detect()
  215. self.bindir = self.get_pkgconfig_host_bins(core)
  216. if not self.bindir:
  217. # If exec_prefix is not defined, the pkg-config file is broken
  218. prefix = core.get_pkgconfig_variable('exec_prefix', {})
  219. if prefix:
  220. self.bindir = os.path.join(prefix, 'bin')
  221. def _find_qmake(self, qmake):
  222. # Even when cross-compiling, if we don't get a cross-info qmake, we
  223. # fallback to using the qmake in PATH because that's what we used to do
  224. if self.env.is_cross_build():
  225. qmake = self.env.cross_info.config['binaries'].get('qmake', qmake)
  226. return ExternalProgram(qmake, silent=True)
  227. def _qmake_detect(self, mods, kwargs):
  228. for qmake in ('qmake-' + self.name, 'qmake'):
  229. self.qmake = self._find_qmake(qmake)
  230. if not self.qmake.found():
  231. continue
  232. # Check that the qmake is for qt5
  233. pc, stdo = Popen_safe(self.qmake.get_command() + ['-v'])[0:2]
  234. if pc.returncode != 0:
  235. continue
  236. if not 'Qt version ' + self.qtver in stdo:
  237. mlog.log('QMake is not for ' + self.qtname)
  238. continue
  239. # Found qmake for Qt5!
  240. break
  241. else:
  242. # Didn't find qmake :(
  243. self.is_found = False
  244. return
  245. self.version = re.search(self.qtver + '(\.\d+)+', stdo).group(0)
  246. # Query library path, header path, and binary path
  247. mlog.log("Found qmake:", mlog.bold(self.qmake.get_name()), '(%s)' % self.version)
  248. stdo = Popen_safe(self.qmake.get_command() + ['-query'])[1]
  249. qvars = {}
  250. for line in stdo.split('\n'):
  251. line = line.strip()
  252. if line == '':
  253. continue
  254. (k, v) = tuple(line.split(':', 1))
  255. qvars[k] = v
  256. if mesonlib.is_osx():
  257. return self._framework_detect(qvars, mods, kwargs)
  258. incdir = qvars['QT_INSTALL_HEADERS']
  259. self.compile_args.append('-I' + incdir)
  260. libdir = qvars['QT_INSTALL_LIBS']
  261. # Used by self.compilers_detect()
  262. self.bindir = self.get_qmake_host_bins(qvars)
  263. self.is_found = True
  264. for module in mods:
  265. mincdir = os.path.join(incdir, 'Qt' + module)
  266. self.compile_args.append('-I' + mincdir)
  267. if for_windows(self.env.is_cross_build(), self.env):
  268. is_debug = self.env.cmd_line_options.buildtype.startswith('debug')
  269. dbg = 'd' if is_debug else ''
  270. if self.qtver == '4':
  271. base_name = 'Qt' + module + dbg + '4'
  272. else:
  273. base_name = 'Qt5' + module + dbg
  274. libfile = os.path.join(libdir, base_name + '.lib')
  275. if not os.path.isfile(libfile):
  276. # MinGW can link directly to .dll
  277. libfile = os.path.join(self.bindir, base_name + '.dll')
  278. if not os.path.isfile(libfile):
  279. self.is_found = False
  280. break
  281. else:
  282. libfile = os.path.join(libdir, 'lib{}{}.so'.format(self.qtpkgname, module))
  283. if not os.path.isfile(libfile):
  284. self.is_found = False
  285. break
  286. self.link_args.append(libfile)
  287. return qmake
  288. def _framework_detect(self, qvars, modules, kwargs):
  289. libdir = qvars['QT_INSTALL_LIBS']
  290. for m in modules:
  291. fname = 'Qt' + m
  292. fwdep = ExtraFrameworkDependency(fname, False, libdir, self.env,
  293. self.language, kwargs)
  294. self.compile_args.append('-F' + libdir)
  295. if fwdep.found():
  296. self.is_found = True
  297. self.compile_args += fwdep.get_compile_args()
  298. self.link_args += fwdep.get_link_args()
  299. # Used by self.compilers_detect()
  300. self.bindir = self.get_qmake_host_bins(qvars)
  301. def get_qmake_host_bins(self, qvars):
  302. # Prefer QT_HOST_BINS (qt5, correct for cross and native compiling)
  303. # but fall back to QT_INSTALL_BINS (qt4)
  304. if 'QT_HOST_BINS' in qvars:
  305. return qvars['QT_HOST_BINS']
  306. else:
  307. return qvars['QT_INSTALL_BINS']
  308. def get_methods(self):
  309. return [DependencyMethods.PKGCONFIG, DependencyMethods.QMAKE]
  310. def get_exe_args(self, compiler):
  311. # Originally this was -fPIE but nowadays the default
  312. # for upstream and distros seems to be -reduce-relocations
  313. # which requires -fPIC. This may cause a performance
  314. # penalty when using self-built Qt or on platforms
  315. # where -fPIC is not required. If this is an issue
  316. # for you, patches are welcome.
  317. return compiler.get_pic_args()
  318. class Qt4Dependency(QtBaseDependency):
  319. def __init__(self, env, kwargs):
  320. QtBaseDependency.__init__(self, 'qt4', env, kwargs)
  321. def get_pkgconfig_host_bins(self, core):
  322. # Only return one bins dir, because the tools are generally all in one
  323. # directory for Qt4, in Qt5, they must all be in one directory. Return
  324. # the first one found among the bin variables, in case one tool is not
  325. # configured to be built.
  326. applications = ['moc', 'uic', 'rcc', 'lupdate', 'lrelease']
  327. for application in applications:
  328. try:
  329. return os.path.dirname(core.get_pkgconfig_variable('%s_location' % application, {}))
  330. except MesonException:
  331. pass
  332. class Qt5Dependency(QtBaseDependency):
  333. def __init__(self, env, kwargs):
  334. QtBaseDependency.__init__(self, 'qt5', env, kwargs)
  335. def get_pkgconfig_host_bins(self, core):
  336. return core.get_pkgconfig_variable('host_bins', {})
  337. # There are three different ways of depending on SDL2:
  338. # sdl2-config, pkg-config and OSX framework
  339. class SDL2Dependency(ExternalDependency):
  340. def __init__(self, environment, kwargs):
  341. super().__init__('sdl2', environment, None, kwargs)
  342. kwargs['required'] = False
  343. if DependencyMethods.PKGCONFIG in self.methods:
  344. try:
  345. pcdep = PkgConfigDependency('sdl2', environment, kwargs)
  346. if pcdep.found():
  347. self.type_name = 'pkgconfig'
  348. self.is_found = True
  349. self.compile_args = pcdep.get_compile_args()
  350. self.link_args = pcdep.get_link_args()
  351. self.version = pcdep.get_version()
  352. return
  353. except Exception as e:
  354. mlog.debug('SDL 2 not found via pkgconfig. Trying next, error was:', str(e))
  355. if DependencyMethods.CONFIG_TOOL in self.methods:
  356. try:
  357. ctdep = ConfigToolDependency.factory(
  358. 'sdl2', environment, None, kwargs, ['sdl2-config'], 'sdl2-config')
  359. if ctdep.found():
  360. self.type_name = 'config-tool'
  361. self.config = ctdep.config
  362. self.version = ctdep.version
  363. self.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
  364. self.links_args = ctdep.get_config_value(['--libs'], 'link_args')
  365. self.is_found = True
  366. return
  367. except Exception as e:
  368. mlog.debug('SDL 2 not found via sdl2-config. Trying next, error was:', str(e))
  369. if DependencyMethods.EXTRAFRAMEWORK in self.methods:
  370. if mesonlib.is_osx():
  371. fwdep = ExtraFrameworkDependency('sdl2', False, None, self.env,
  372. self.language, kwargs)
  373. if fwdep.found():
  374. self.is_found = True
  375. self.compile_args = fwdep.get_compile_args()
  376. self.link_args = fwdep.get_link_args()
  377. self.version = '2' # FIXME
  378. return
  379. mlog.log('Dependency', mlog.bold('sdl2'), 'found:', mlog.red('NO'))
  380. def get_methods(self):
  381. if mesonlib.is_osx():
  382. return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK]
  383. else:
  384. return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL]
  385. class WxDependency(ConfigToolDependency):
  386. tools = ['wx-config-3.0', 'wx-config']
  387. tool_name = 'wx-config'
  388. def __init__(self, environment, kwargs):
  389. super().__init__('WxWidgets', environment, None, kwargs)
  390. if not self.is_found:
  391. return
  392. self.requested_modules = self.get_requested(kwargs)
  393. # wx-config seems to have a cflags as well but since it requires C++,
  394. # this should be good, at least for now.
  395. self.compile_args = self.get_config_value(['--cxxflags'], 'compile_args')
  396. self.link_args = self.get_config_value(['--libs'], 'link_args')
  397. def get_requested(self, kwargs):
  398. if 'modules' not in kwargs:
  399. return []
  400. candidates = extract_as_list(kwargs, 'modules')
  401. for c in candidates:
  402. if not isinstance(c, str):
  403. raise DependencyException('wxwidgets module argument is not a string')
  404. return candidates
  405. class VulkanDependency(ExternalDependency):
  406. def __init__(self, environment, kwargs):
  407. super().__init__('vulkan', environment, None, kwargs)
  408. if DependencyMethods.PKGCONFIG in self.methods:
  409. try:
  410. pcdep = PkgConfigDependency('vulkan', environment, kwargs)
  411. if pcdep.found():
  412. self.type_name = 'pkgconfig'
  413. self.is_found = True
  414. self.compile_args = pcdep.get_compile_args()
  415. self.link_args = pcdep.get_link_args()
  416. self.version = pcdep.get_version()
  417. return
  418. except Exception:
  419. pass
  420. if DependencyMethods.SYSTEM in self.methods:
  421. try:
  422. self.vulkan_sdk = os.environ['VULKAN_SDK']
  423. if not os.path.isabs(self.vulkan_sdk):
  424. raise DependencyException('VULKAN_SDK must be an absolute path.')
  425. except KeyError:
  426. self.vulkan_sdk = None
  427. if self.vulkan_sdk:
  428. # TODO: this config might not work on some platforms, fix bugs as reported
  429. # we should at least detect other 64-bit platforms (e.g. armv8)
  430. lib_name = 'vulkan'
  431. if mesonlib.is_windows():
  432. lib_name = 'vulkan-1'
  433. lib_dir = 'Lib32'
  434. inc_dir = 'Include'
  435. if detect_cpu({}) == 'x86_64':
  436. lib_dir = 'Lib'
  437. else:
  438. lib_name = 'vulkan'
  439. lib_dir = 'lib'
  440. inc_dir = 'include'
  441. # make sure header and lib are valid
  442. inc_path = os.path.join(self.vulkan_sdk, inc_dir)
  443. header = os.path.join(inc_path, 'vulkan', 'vulkan.h')
  444. lib_path = os.path.join(self.vulkan_sdk, lib_dir)
  445. find_lib = self.compiler.find_library(lib_name, environment, lib_path)
  446. if not find_lib:
  447. raise DependencyException('VULKAN_SDK point to invalid directory (no lib)')
  448. if not os.path.isfile(header):
  449. raise DependencyException('VULKAN_SDK point to invalid directory (no include)')
  450. self.type_name = 'vulkan_sdk'
  451. self.is_found = True
  452. self.compile_args.append('-I' + inc_path)
  453. self.link_args.append('-L' + lib_path)
  454. self.link_args.append('-l' + lib_name)
  455. # TODO: find a way to retrieve the version from the sdk?
  456. # Usually it is a part of the path to it (but does not have to be)
  457. self.version = '1'
  458. return
  459. else:
  460. # simply try to guess it, usually works on linux
  461. libs = self.compiler.find_library('vulkan', environment, [])
  462. if libs is not None and self.compiler.has_header('vulkan/vulkan.h', '', environment):
  463. self.type_name = 'system'
  464. self.is_found = True
  465. self.version = 1 # TODO
  466. for lib in libs:
  467. self.link_args.append(lib)
  468. return
  469. def get_methods(self):
  470. return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]