qt5.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. # Thomas Nagy, 2006-2018 (ita)
  4. # Rafaël Kooi, 2023 (RA-Kooi)
  5. """
  6. This tool helps with finding Qt5 and Qt6 tools and libraries,
  7. and also provides syntactic sugar for using Qt5 and Qt6 tools.
  8. The following snippet illustrates the tool usage::
  9. def options(opt):
  10. opt.load('compiler_cxx qt5')
  11. def configure(conf):
  12. conf.load('compiler_cxx qt5')
  13. def build(bld):
  14. bld(
  15. features = 'qt5 cxx cxxprogram',
  16. uselib = 'QT5CORE QT5GUI QT5OPENGL QT5SVG',
  17. source = 'main.cpp textures.qrc aboutDialog.ui',
  18. target = 'window',
  19. )
  20. Alternatively the following snippet illustrates Qt6 tool usage::
  21. def options(opt):
  22. opt.load('compiler_cxx qt5')
  23. def configure(conf):
  24. conf.want_qt6 = True
  25. conf.load('compiler_cxx qt5')
  26. def build(bld):
  27. bld(
  28. features = 'qt6 cxx cxxprogram',
  29. uselib = 'QT6CORE QT6GUI QT6OPENGL QT6SVG',
  30. source = 'main.cpp textures.qrc aboutDialog.ui',
  31. target = 'window',
  32. )
  33. Here, the UI description and resource files will be processed
  34. to generate code.
  35. Usage
  36. =====
  37. Load the "qt5" tool.
  38. You also need to edit your sources accordingly:
  39. - the normal way of doing things is to have your C++ files
  40. include the .moc file.
  41. This is regarded as the best practice (and provides much faster
  42. compilations).
  43. It also implies that the include paths have beenset properly.
  44. - to have the include paths added automatically, use the following::
  45. from waflib.TaskGen import feature, before_method, after_method
  46. @feature('cxx')
  47. @after_method('process_source')
  48. @before_method('apply_incpaths')
  49. def add_includes_paths(self):
  50. incs = set(self.to_list(getattr(self, 'includes', '')))
  51. for x in self.compiled_tasks:
  52. incs.add(x.inputs[0].parent.path_from(self.path))
  53. self.includes = sorted(incs)
  54. Note: another tool provides Qt processing that does not require
  55. .moc includes, see 'playground/slow_qt/'.
  56. A few options (--qt{dir,bin,...}) and environment variables
  57. (QT5_{ROOT,DIR,MOC,UIC,XCOMPILE}) allow finer tuning of the tool,
  58. tool path selection, etc; please read the source for more info.
  59. For Qt6 replace the QT5_ prefix with QT6_.
  60. The detection uses pkg-config on Linux by default. The list of
  61. libraries to be requested to pkg-config is formulated by scanning
  62. in the QTLIBS directory (that can be passed via --qtlibs or by
  63. setting the environment variable QT5_LIBDIR or QT6_LIBDIR otherwise is
  64. derived by querying qmake for QT_INSTALL_LIBS directory) for
  65. shared/static libraries present.
  66. Alternatively the list of libraries to be requested via pkg-config
  67. can be set using the qt5_vars attribute, ie:
  68. conf.qt5_vars = ['Qt5Core', 'Qt5Gui', 'Qt5Widgets', 'Qt5Test'];
  69. For Qt6 use the qt6_vars attribute.
  70. This can speed up configuration phase if needed libraries are
  71. known beforehand, can improve detection on systems with a
  72. sparse QT5/Qt6 libraries installation (ie. NIX) and can improve
  73. detection of some header-only Qt modules (ie. Qt5UiPlugin).
  74. To force static library detection use:
  75. QT5_XCOMPILE=1 QT5_FORCE_STATIC=1 waf configure
  76. To use Qt6 set the want_qt6 attribute, ie:
  77. conf.want_qt6 = True;
  78. """
  79. try:
  80. from xml.sax import make_parser
  81. from xml.sax.handler import ContentHandler
  82. except ImportError:
  83. has_xml = False
  84. ContentHandler = object
  85. else:
  86. has_xml = True
  87. import os, sys, re
  88. from waflib.Tools import cxx
  89. from waflib import Build, Task, Utils, Options, Errors, Context
  90. from waflib.TaskGen import feature, after_method, extension, before_method
  91. from waflib.Configure import conf
  92. from waflib import Logs
  93. MOC_H = ['.h', '.hpp', '.hxx', '.hh']
  94. """
  95. File extensions associated to .moc files
  96. """
  97. EXT_RCC = ['.qrc']
  98. """
  99. File extension for the resource (.qrc) files
  100. """
  101. EXT_UI = ['.ui']
  102. """
  103. File extension for the user interface (.ui) files
  104. """
  105. EXT_QT5 = ['.cpp', '.cc', '.cxx', '.C']
  106. """
  107. File extensions of C++ files that may require a .moc processing
  108. """
  109. class qxx(Task.classes['cxx']):
  110. """
  111. Each C++ file can have zero or several .moc files to create.
  112. They are known only when the files are scanned (preprocessor)
  113. To avoid scanning the c++ files each time (parsing C/C++), the results
  114. are retrieved from the task cache (bld.node_deps/bld.raw_deps).
  115. The moc tasks are also created *dynamically* during the build.
  116. """
  117. def __init__(self, *k, **kw):
  118. Task.Task.__init__(self, *k, **kw)
  119. self.moc_done = 0
  120. def runnable_status(self):
  121. """
  122. Compute the task signature to make sure the scanner was executed. Create the
  123. moc tasks by using :py:meth:`waflib.Tools.qt5.qxx.add_moc_tasks` (if necessary),
  124. then postpone the task execution (there is no need to recompute the task signature).
  125. """
  126. if self.moc_done:
  127. return Task.Task.runnable_status(self)
  128. else:
  129. for t in self.run_after:
  130. if not t.hasrun:
  131. return Task.ASK_LATER
  132. self.add_moc_tasks()
  133. return Task.Task.runnable_status(self)
  134. def create_moc_task(self, h_node, m_node):
  135. """
  136. If several libraries use the same classes, it is possible that moc will run several times (Issue 1318)
  137. It is not possible to change the file names, but we can assume that the moc transformation will be identical,
  138. and the moc tasks can be shared in a global cache.
  139. """
  140. try:
  141. moc_cache = self.generator.bld.moc_cache
  142. except AttributeError:
  143. moc_cache = self.generator.bld.moc_cache = {}
  144. try:
  145. return moc_cache[h_node]
  146. except KeyError:
  147. tsk = moc_cache[h_node] = Task.classes['moc'](env=self.env, generator=self.generator)
  148. tsk.set_inputs(h_node)
  149. tsk.set_outputs(m_node)
  150. tsk.env.append_unique('MOC_FLAGS', '-i')
  151. if self.generator:
  152. self.generator.tasks.append(tsk)
  153. # direct injection in the build phase (safe because called from the main thread)
  154. gen = self.generator.bld.producer
  155. gen.outstanding.append(tsk)
  156. gen.total += 1
  157. return tsk
  158. else:
  159. # remove the signature, it must be recomputed with the moc task
  160. delattr(self, 'cache_sig')
  161. def add_moc_tasks(self):
  162. """
  163. Creates moc tasks by looking in the list of file dependencies ``bld.raw_deps[self.uid()]``
  164. """
  165. node = self.inputs[0]
  166. bld = self.generator.bld
  167. # skip on uninstall due to generated files
  168. if bld.is_install == Build.UNINSTALL:
  169. return
  170. try:
  171. # compute the signature once to know if there is a moc file to create
  172. self.signature()
  173. except KeyError:
  174. # the moc file may be referenced somewhere else
  175. pass
  176. else:
  177. # remove the signature, it must be recomputed with the moc task
  178. delattr(self, 'cache_sig')
  179. include_nodes = [node.parent] + self.generator.includes_nodes
  180. moctasks = []
  181. mocfiles = set()
  182. for d in bld.raw_deps.get(self.uid(), []):
  183. if not d.endswith('.moc'):
  184. continue
  185. # process that base.moc only once
  186. if d in mocfiles:
  187. continue
  188. mocfiles.add(d)
  189. # find the source associated with the moc file
  190. h_node = None
  191. base2 = d[:-4]
  192. # foo.moc from foo.cpp
  193. prefix = node.name[:node.name.rfind('.')]
  194. if base2 == prefix:
  195. h_node = node
  196. else:
  197. # this deviates from the standard
  198. # if bar.cpp includes foo.moc, then assume it is from foo.h
  199. for x in include_nodes:
  200. for e in MOC_H:
  201. h_node = x.find_node(base2 + e)
  202. if h_node:
  203. break
  204. else:
  205. continue
  206. break
  207. if h_node:
  208. m_node = h_node.change_ext('.moc')
  209. else:
  210. raise Errors.WafError('No source found for %r which is a moc file' % d)
  211. # create the moc task
  212. task = self.create_moc_task(h_node, m_node)
  213. moctasks.append(task)
  214. # simple scheduler dependency: run the moc task before others
  215. self.run_after.update(set(moctasks))
  216. self.moc_done = 1
  217. class trans_update(Task.Task):
  218. """Updates a .ts files from a list of C++ files"""
  219. run_str = '${QT_LUPDATE} ${SRC} -ts ${TGT}'
  220. color = 'BLUE'
  221. class XMLHandler(ContentHandler):
  222. """
  223. Parses ``.qrc`` files
  224. """
  225. def __init__(self):
  226. ContentHandler.__init__(self)
  227. self.buf = []
  228. self.files = []
  229. def startElement(self, name, attrs):
  230. if name == 'file':
  231. self.buf = []
  232. def endElement(self, name):
  233. if name == 'file':
  234. self.files.append(str(''.join(self.buf)))
  235. def characters(self, cars):
  236. self.buf.append(cars)
  237. @extension(*EXT_RCC)
  238. def create_rcc_task(self, node):
  239. "Creates rcc and cxx tasks for ``.qrc`` files"
  240. rcnode = node.change_ext('_rc.%d.cpp' % self.idx)
  241. self.create_task('rcc', node, rcnode)
  242. cpptask = self.create_task('cxx', rcnode, rcnode.change_ext('.o'))
  243. try:
  244. self.compiled_tasks.append(cpptask)
  245. except AttributeError:
  246. self.compiled_tasks = [cpptask]
  247. return cpptask
  248. @extension(*EXT_UI)
  249. def create_uic_task(self, node):
  250. "Create uic tasks for user interface ``.ui`` definition files"
  251. """
  252. If UIC file is used in more than one bld, we would have a conflict in parallel execution
  253. It is not possible to change the file names (like .self.idx. as for objects) as they have
  254. to be referenced by the source file, but we can assume that the transformation will be identical
  255. and the tasks can be shared in a global cache.
  256. """
  257. try:
  258. uic_cache = self.bld.uic_cache
  259. except AttributeError:
  260. uic_cache = self.bld.uic_cache = {}
  261. if node not in uic_cache:
  262. uictask = uic_cache[node] = self.create_task('ui5', node)
  263. uictask.outputs = [node.parent.find_or_declare(self.env.ui_PATTERN % node.name[:-3])]
  264. @extension('.ts')
  265. def add_lang(self, node):
  266. """Adds all the .ts file into ``self.lang``"""
  267. self.lang = self.to_list(getattr(self, 'lang', [])) + [node]
  268. @feature('qt5', 'qt6')
  269. @before_method('process_source')
  270. def process_mocs(self):
  271. """
  272. Processes MOC files included in headers::
  273. def build(bld):
  274. bld.program(features='qt5', source='main.cpp', target='app', use='QT5CORE', moc='foo.h')
  275. The build will run moc on foo.h to create moc_foo.n.cpp. The number in the file name
  276. is provided to avoid name clashes when the same headers are used by several targets.
  277. """
  278. lst = self.to_nodes(getattr(self, 'moc', []))
  279. self.source = self.to_list(getattr(self, 'source', []))
  280. for x in lst:
  281. prefix = x.name[:x.name.rfind('.')] # foo.h -> foo
  282. moc_target = 'moc_%s.%d.cpp' % (prefix, self.idx)
  283. moc_node = x.parent.find_or_declare(moc_target)
  284. self.source.append(moc_node)
  285. self.create_task('moc', x, moc_node)
  286. @feature('qt5', 'qt6')
  287. @after_method('apply_link')
  288. def apply_qt5(self):
  289. """
  290. Adds MOC_FLAGS which may be necessary for moc::
  291. def build(bld):
  292. bld.program(features='qt5', source='main.cpp', target='app', use='QT5CORE')
  293. The additional parameters are:
  294. :param lang: list of translation files (\\*.ts) to process
  295. :type lang: list of :py:class:`waflib.Node.Node` or string without the .ts extension
  296. :param update: whether to process the C++ files to update the \\*.ts files (use **waf --translate**)
  297. :type update: bool
  298. :param langname: if given, transform the \\*.ts files into a .qrc files to include in the binary file
  299. :type langname: :py:class:`waflib.Node.Node` or string without the .qrc extension
  300. """
  301. if getattr(self, 'lang', None):
  302. qmtasks = []
  303. for x in self.to_list(self.lang):
  304. if isinstance(x, str):
  305. x = self.path.find_resource(x + '.ts')
  306. qmtasks.append(self.create_task('ts2qm', x, x.change_ext('.%d.qm' % self.idx)))
  307. if getattr(self, 'update', None) and Options.options.trans_qt5:
  308. cxxnodes = [a.inputs[0] for a in self.compiled_tasks] + [
  309. a.inputs[0] for a in self.tasks if a.inputs and a.inputs[0].name.endswith('.ui')]
  310. for x in qmtasks:
  311. self.create_task('trans_update', cxxnodes, x.inputs)
  312. if getattr(self, 'langname', None):
  313. qmnodes = [x.outputs[0] for x in qmtasks]
  314. rcnode = self.langname
  315. if isinstance(rcnode, str):
  316. rcnode = self.path.find_or_declare(rcnode + ('.%d.qrc' % self.idx))
  317. t = self.create_task('qm2rcc', qmnodes, rcnode)
  318. k = create_rcc_task(self, t.outputs[0])
  319. self.link_task.inputs.append(k.outputs[0])
  320. lst = []
  321. for flag in self.to_list(self.env.CXXFLAGS):
  322. if len(flag) < 2:
  323. continue
  324. f = flag[0:2]
  325. if f in ('-D', '-I', '/D', '/I'):
  326. if (f[0] == '/'):
  327. lst.append('-' + flag[1:])
  328. else:
  329. lst.append(flag)
  330. self.env.append_value('MOC_FLAGS', lst)
  331. @extension(*EXT_QT5)
  332. def cxx_hook(self, node):
  333. """
  334. Re-maps C++ file extensions to the :py:class:`waflib.Tools.qt5.qxx` task.
  335. """
  336. return self.create_compiled_task('qxx', node)
  337. class rcc(Task.Task):
  338. """
  339. Processes ``.qrc`` files
  340. """
  341. color = 'BLUE'
  342. run_str = '${QT_RCC} -name ${tsk.rcname()} ${SRC[0].abspath()} ${RCC_ST} -o ${TGT}'
  343. ext_out = ['.h']
  344. def rcname(self):
  345. return os.path.splitext(self.inputs[0].name)[0]
  346. def scan(self):
  347. """Parse the *.qrc* files"""
  348. if not has_xml:
  349. Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!')
  350. return ([], [])
  351. parser = make_parser()
  352. curHandler = XMLHandler()
  353. parser.setContentHandler(curHandler)
  354. with open(self.inputs[0].abspath(), 'r') as f:
  355. parser.parse(f)
  356. nodes = []
  357. names = []
  358. root = self.inputs[0].parent
  359. for x in curHandler.files:
  360. nd = root.find_resource(x)
  361. if nd:
  362. nodes.append(nd)
  363. else:
  364. names.append(x)
  365. return (nodes, names)
  366. def quote_flag(self, x):
  367. """
  368. Override Task.quote_flag. QT parses the argument files
  369. differently than cl.exe and link.exe
  370. :param x: flag
  371. :type x: string
  372. :return: quoted flag
  373. :rtype: string
  374. """
  375. return x
  376. class moc(Task.Task):
  377. """
  378. Creates ``.moc`` files
  379. """
  380. color = 'BLUE'
  381. run_str = '${QT_MOC} ${MOC_FLAGS} ${MOCCPPPATH_ST:INCPATHS} ${MOCDEFINES_ST:DEFINES} ${SRC} ${MOC_ST} ${TGT}'
  382. def quote_flag(self, x):
  383. """
  384. Override Task.quote_flag. QT parses the argument files
  385. differently than cl.exe and link.exe
  386. :param x: flag
  387. :type x: string
  388. :return: quoted flag
  389. :rtype: string
  390. """
  391. return x
  392. class ui5(Task.Task):
  393. """
  394. Processes ``.ui`` files
  395. """
  396. color = 'BLUE'
  397. run_str = '${QT_UIC} ${SRC} -o ${TGT}'
  398. ext_out = ['.h']
  399. class ts2qm(Task.Task):
  400. """
  401. Generates ``.qm`` files from ``.ts`` files
  402. """
  403. color = 'BLUE'
  404. run_str = '${QT_LRELEASE} ${QT_LRELEASE_FLAGS} ${SRC} -qm ${TGT}'
  405. class qm2rcc(Task.Task):
  406. """
  407. Generates ``.qrc`` files from ``.qm`` files
  408. """
  409. color = 'BLUE'
  410. after = 'ts2qm'
  411. def run(self):
  412. """Create a qrc file including the inputs"""
  413. txt = '\n'.join(['<file>%s</file>' % k.path_from(self.outputs[0].parent) for k in self.inputs])
  414. code = '<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n%s\n</qresource>\n</RCC>' % txt
  415. self.outputs[0].write(code)
  416. def configure(self):
  417. """
  418. Besides the configuration options, the environment variable QT5_ROOT may be used
  419. to give the location of the qt5 libraries (absolute path).
  420. The detection uses the program ``pkg-config`` through :py:func:`waflib.Tools.config_c.check_cfg`
  421. """
  422. if 'COMPILER_CXX' not in self.env:
  423. self.fatal('No CXX compiler defined: did you forget to configure compiler_cxx first?')
  424. self.want_qt6 = getattr(self, 'want_qt6', False)
  425. if self.want_qt6:
  426. self.qt_vars = Utils.to_list(getattr(self, 'qt6_vars', []))
  427. else:
  428. self.qt_vars = Utils.to_list(getattr(self, 'qt5_vars', []))
  429. self.find_qt5_binaries()
  430. self.set_qt5_libs_dir()
  431. self.set_qt5_libs_to_check()
  432. self.set_qt5_defines()
  433. self.find_qt5_libraries()
  434. self.add_qt5_rpath()
  435. self.simplify_qt5_libs()
  436. # warn about this during the configuration too
  437. if not has_xml:
  438. Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!')
  439. feature = 'qt6' if self.want_qt6 else 'qt5'
  440. # Qt5 may be compiled with '-reduce-relocations' which requires dependent programs to have -fPIE or -fPIC?
  441. frag = '#include <QMap>\nint main(int argc, char **argv) {QMap<int,int> m;return m.keys().size();}\n'
  442. uses = 'QT6CORE' if self.want_qt6 else 'QT5CORE'
  443. # Qt6 requires C++17 (https://www.qt.io/blog/qt-6.0-released)
  444. flag_list = []
  445. if self.env.CXX_NAME == 'msvc':
  446. stdflag = '/std:c++17' if self.want_qt6 else '/std:c++11'
  447. flag_list = [[], ['/Zc:__cplusplus', '/permissive-', stdflag]]
  448. else:
  449. stdflag = '-std=c++17' if self.want_qt6 else '-std=c++11'
  450. flag_list = [[], '-fPIE', '-fPIC', stdflag, [stdflag, '-fPIE'], [stdflag, '-fPIC']]
  451. for flag in flag_list:
  452. msg = 'See if Qt files compile '
  453. if flag:
  454. msg += 'with %s' % flag
  455. try:
  456. self.check(features=feature + ' cxx', use=uses, uselib_store=feature, cxxflags=flag, fragment=frag, msg=msg)
  457. except self.errors.ConfigurationError:
  458. pass
  459. else:
  460. break
  461. else:
  462. self.fatal('Could not build a simple Qt application')
  463. # FreeBSD does not add /usr/local/lib and the pkg-config files do not provide it either :-/
  464. if Utils.unversioned_sys_platform() == 'freebsd':
  465. frag = '#include <QMap>\nint main(int argc, char **argv) {QMap<int,int> m;return m.keys().size();}\n'
  466. try:
  467. self.check(features=feature + ' cxx cxxprogram', use=uses, fragment=frag, msg='Can we link Qt programs on FreeBSD directly?')
  468. except self.errors.ConfigurationError:
  469. self.check(features=feature + ' cxx cxxprogram', use=uses, uselib_store=feature, libpath='/usr/local/lib', fragment=frag, msg='Is /usr/local/lib required?')
  470. @conf
  471. def find_qt5_binaries(self):
  472. """
  473. Detects Qt programs such as qmake, moc, uic, lrelease
  474. """
  475. env = self.env
  476. opt = Options.options
  477. qtdir = getattr(opt, 'qtdir', '')
  478. qtbin = getattr(opt, 'qtbin', '')
  479. qt_ver = '6' if self.want_qt6 else '5'
  480. paths = []
  481. if qtdir:
  482. qtbin = os.path.join(qtdir, 'bin')
  483. # the qt directory has been given from QT5_ROOT - deduce the qt binary path
  484. if not qtdir:
  485. qtdir = self.environ.get('QT' + qt_ver + '_ROOT', '')
  486. qtbin = self.environ.get('QT' + qt_ver + '_BIN') or os.path.join(qtdir, 'bin')
  487. if qtbin:
  488. paths = [qtbin]
  489. # no qtdir, look in the path and in /usr/local/Trolltech
  490. if not qtdir:
  491. paths = self.environ.get('PATH', '').split(os.pathsep)
  492. paths.extend([
  493. '/usr/share/qt' + qt_ver + '/bin',
  494. '/usr/local/lib/qt' + qt_ver + '/bin'])
  495. try:
  496. lst = Utils.listdir('/usr/local/Trolltech/')
  497. except OSError:
  498. pass
  499. else:
  500. if lst:
  501. lst.sort()
  502. lst.reverse()
  503. # keep the highest version
  504. qtdir = '/usr/local/Trolltech/%s/' % lst[0]
  505. qtbin = os.path.join(qtdir, 'bin')
  506. paths.append(qtbin)
  507. # at the end, try to find qmake in the paths given
  508. # keep the one with the highest version
  509. cand = None
  510. prev_ver = ['0', '0', '0']
  511. qmake_vars = ['qmake-qt' + qt_ver, 'qmake' + qt_ver, 'qmake']
  512. for qmk in qmake_vars:
  513. try:
  514. qmake = self.find_program(qmk, path_list=paths)
  515. except self.errors.ConfigurationError:
  516. pass
  517. else:
  518. try:
  519. version = self.cmd_and_log(qmake + ['-query', 'QT_VERSION']).strip()
  520. except self.errors.WafError:
  521. pass
  522. else:
  523. if version:
  524. new_ver = version.split('.')
  525. if new_ver[0] == qt_ver and new_ver > prev_ver:
  526. cand = qmake
  527. prev_ver = new_ver
  528. # qmake could not be found easily, rely on qtchooser
  529. if not cand:
  530. try:
  531. self.find_program('qtchooser')
  532. except self.errors.ConfigurationError:
  533. pass
  534. else:
  535. cmd = self.env.QTCHOOSER + ['-qt=' + qt_ver, '-run-tool=qmake']
  536. try:
  537. version = self.cmd_and_log(cmd + ['-query', 'QT_VERSION'])
  538. except self.errors.WafError:
  539. pass
  540. else:
  541. cand = cmd
  542. if cand:
  543. self.env.QMAKE = cand
  544. else:
  545. self.fatal('Could not find qmake for qt' + qt_ver)
  546. # Once we have qmake, we want to query qmake for the paths where we want to look for tools instead
  547. paths = []
  548. self.env.QT_HOST_BINS = qtbin = self.cmd_and_log(self.env.QMAKE + ['-query', 'QT_HOST_BINS']).strip()
  549. paths.append(qtbin)
  550. if self.want_qt6:
  551. self.env.QT_HOST_LIBEXECS = self.cmd_and_log(self.env.QMAKE + ['-query', 'QT_HOST_LIBEXECS']).strip()
  552. paths.append(self.env.QT_HOST_LIBEXECS)
  553. def find_bin(lst, var):
  554. if var in env:
  555. return
  556. for f in lst:
  557. try:
  558. ret = self.find_program(f, path_list=paths)
  559. except self.errors.ConfigurationError:
  560. pass
  561. else:
  562. env[var]=ret
  563. break
  564. find_bin(['uic-qt' + qt_ver, 'uic'], 'QT_UIC')
  565. if not env.QT_UIC:
  566. self.fatal('cannot find the uic compiler for qt' + qt_ver)
  567. self.start_msg('Checking for uic version')
  568. uicver = self.cmd_and_log(env.QT_UIC + ['-version'], output=Context.BOTH)
  569. uicver = ''.join(uicver).strip()
  570. uicver = uicver.replace('Qt User Interface Compiler ','').replace('User Interface Compiler for Qt', '')
  571. self.end_msg(uicver)
  572. if uicver.find(' 3.') != -1 or uicver.find(' 4.') != -1 or (self.want_qt6 and uicver.find(' 5.') != -1):
  573. if self.want_qt6:
  574. self.fatal('this uic compiler is for qt3 or qt4 or qt5, add uic for qt6 to your path')
  575. else:
  576. self.fatal('this uic compiler is for qt3 or qt4, add uic for qt5 to your path')
  577. find_bin(['moc-qt' + qt_ver, 'moc'], 'QT_MOC')
  578. find_bin(['rcc-qt' + qt_ver, 'rcc'], 'QT_RCC')
  579. find_bin(['lrelease-qt' + qt_ver, 'lrelease'], 'QT_LRELEASE')
  580. find_bin(['lupdate-qt' + qt_ver, 'lupdate'], 'QT_LUPDATE')
  581. env.UIC_ST = '%s -o %s'
  582. env.MOC_ST = '-o'
  583. env.ui_PATTERN = 'ui_%s.h'
  584. env.QT_LRELEASE_FLAGS = ['-silent']
  585. env.MOCCPPPATH_ST = '-I%s'
  586. env.MOCDEFINES_ST = '-D%s'
  587. @conf
  588. def set_qt5_libs_dir(self):
  589. env = self.env
  590. qt_ver = '6' if self.want_qt6 else '5'
  591. qtlibs = getattr(Options.options, 'qtlibs', None) or self.environ.get('QT' + qt_ver + '_LIBDIR')
  592. if not qtlibs:
  593. try:
  594. qtlibs = self.cmd_and_log(env.QMAKE + ['-query', 'QT_INSTALL_LIBS']).strip()
  595. except Errors.WafError:
  596. qtdir = self.cmd_and_log(env.QMAKE + ['-query', 'QT_INSTALL_PREFIX']).strip()
  597. qtlibs = os.path.join(qtdir, 'lib')
  598. self.msg('Found the Qt' + qt_ver + ' library path', qtlibs)
  599. env.QTLIBS = qtlibs
  600. @conf
  601. def find_single_qt5_lib(self, name, uselib, qtlibs, qtincludes, force_static):
  602. env = self.env
  603. qt_ver = '6' if self.want_qt6 else '5'
  604. if force_static:
  605. exts = ('.a', '.lib')
  606. prefix = 'STLIB'
  607. else:
  608. exts = ('.so', '.lib')
  609. prefix = 'LIB'
  610. def lib_names():
  611. for x in exts:
  612. for k in ('', qt_ver) if Utils.is_win32 else ['']:
  613. for p in ('lib', ''):
  614. yield (p, name, k, x)
  615. for tup in lib_names():
  616. k = ''.join(tup)
  617. path = os.path.join(qtlibs, k)
  618. if os.path.exists(path):
  619. if env.DEST_OS == 'win32':
  620. libval = ''.join(tup[:-1])
  621. else:
  622. libval = name
  623. env.append_unique(prefix + '_' + uselib, libval)
  624. env.append_unique('%sPATH_%s' % (prefix, uselib), qtlibs)
  625. env.append_unique('INCLUDES_' + uselib, qtincludes)
  626. env.append_unique('INCLUDES_' + uselib, os.path.join(qtincludes, name.replace('Qt' + qt_ver, 'Qt')))
  627. return k
  628. return False
  629. @conf
  630. def find_qt5_libraries(self):
  631. env = self.env
  632. qt_ver = '6' if self.want_qt6 else '5'
  633. qtincludes = self.environ.get('QT' + qt_ver + '_INCLUDES') or self.cmd_and_log(env.QMAKE + ['-query', 'QT_INSTALL_HEADERS']).strip()
  634. force_static = self.environ.get('QT' + qt_ver + '_FORCE_STATIC')
  635. try:
  636. if self.environ.get('QT' + qt_ver + '_XCOMPILE'):
  637. self.fatal('QT' + qt_ver + '_XCOMPILE Disables pkg-config detection')
  638. self.check_cfg(atleast_pkgconfig_version='0.1')
  639. except self.errors.ConfigurationError:
  640. for i in self.qt_vars:
  641. uselib = i.upper()
  642. if Utils.unversioned_sys_platform() == 'darwin':
  643. # Since at least qt 4.7.3 each library locates in separate directory
  644. fwk = i.replace('Qt' + qt_ver, 'Qt')
  645. frameworkName = fwk + '.framework'
  646. qtDynamicLib = os.path.join(env.QTLIBS, frameworkName, fwk)
  647. if os.path.exists(qtDynamicLib):
  648. env.append_unique('FRAMEWORK_' + uselib, fwk)
  649. env.append_unique('FRAMEWORKPATH_' + uselib, env.QTLIBS)
  650. self.msg('Checking for %s' % i, qtDynamicLib, 'GREEN')
  651. else:
  652. self.msg('Checking for %s' % i, False, 'YELLOW')
  653. env.append_unique('INCLUDES_' + uselib, os.path.join(env.QTLIBS, frameworkName, 'Headers'))
  654. else:
  655. ret = self.find_single_qt5_lib(i, uselib, env.QTLIBS, qtincludes, force_static)
  656. if not force_static and not ret:
  657. ret = self.find_single_qt5_lib(i, uselib, env.QTLIBS, qtincludes, True)
  658. self.msg('Checking for %s' % i, ret, 'GREEN' if ret else 'YELLOW')
  659. else:
  660. path = '%s:%s:%s/pkgconfig:/usr/lib/qt%s/lib/pkgconfig:/opt/qt%s/lib/pkgconfig:/usr/lib/qt%s/lib:/opt/qt%s/lib' % (
  661. self.environ.get('PKG_CONFIG_PATH', ''), env.QTLIBS, env.QTLIBS, qt_ver, qt_ver, qt_ver, qt_ver)
  662. for i in self.qt_vars:
  663. self.check_cfg(package=i, args='--cflags --libs', mandatory=False, force_static=force_static, pkg_config_path=path)
  664. @conf
  665. def simplify_qt5_libs(self):
  666. """
  667. Since library paths make really long command-lines,
  668. and since everything depends on qtcore, remove the qtcore ones from qtgui, etc
  669. """
  670. env = self.env
  671. def process_lib(vars_, coreval):
  672. for d in vars_:
  673. var = d.upper()
  674. if var == 'QTCORE':
  675. continue
  676. value = env['LIBPATH_'+var]
  677. if value:
  678. core = env[coreval]
  679. accu = []
  680. for lib in value:
  681. if lib in core:
  682. continue
  683. accu.append(lib)
  684. env['LIBPATH_'+var] = accu
  685. process_lib(self.qt_vars, 'LIBPATH_QTCORE')
  686. @conf
  687. def add_qt5_rpath(self):
  688. """
  689. Defines rpath entries for Qt libraries
  690. """
  691. env = self.env
  692. if getattr(Options.options, 'want_rpath', False):
  693. def process_rpath(vars_, coreval):
  694. for d in vars_:
  695. var = d.upper()
  696. value = env['LIBPATH_' + var]
  697. if value:
  698. core = env[coreval]
  699. accu = []
  700. for lib in value:
  701. if var != 'QTCORE':
  702. if lib in core:
  703. continue
  704. accu.append('-Wl,--rpath='+lib)
  705. env['RPATH_' + var] = accu
  706. process_rpath(self.qt_vars, 'LIBPATH_QTCORE')
  707. @conf
  708. def set_qt5_libs_to_check(self):
  709. qt_ver = '6' if self.want_qt6 else '5'
  710. if not self.qt_vars:
  711. dirlst = Utils.listdir(self.env.QTLIBS)
  712. pat = self.env.cxxshlib_PATTERN
  713. if Utils.is_win32:
  714. pat = pat.replace('.dll', '.lib')
  715. if self.environ.get('QT' + qt_ver + '_FORCE_STATIC'):
  716. pat = self.env.cxxstlib_PATTERN
  717. if Utils.unversioned_sys_platform() == 'darwin':
  718. pat = r"%s\.framework"
  719. if self.want_qt6:
  720. # match Qt6Name or QtName but not Qt5Name
  721. mid_pattern = pat % 'Qt6?(?P<name>[^5]\\w+)'
  722. else:
  723. # match Qt5Name or QtName but not Qt6Name
  724. mid_pattern = pat % 'Qt5?(?P<name>[^6]\\w+)'
  725. re_qt = re.compile('^%s$' % mid_pattern)
  726. for x in sorted(dirlst):
  727. m = re_qt.match(x)
  728. if m:
  729. self.qt_vars.append("Qt%s%s" % (qt_ver, m.group('name')))
  730. if not self.qt_vars:
  731. self.fatal('cannot find any Qt%s library (%r)' % (qt_ver, self.env.QTLIBS))
  732. qtextralibs = getattr(Options.options, 'qtextralibs', None)
  733. if qtextralibs:
  734. self.qt_vars.extend(qtextralibs.split(','))
  735. @conf
  736. def set_qt5_defines(self):
  737. qt_ver = '6' if self.want_qt6 else '5'
  738. if sys.platform != 'win32':
  739. return
  740. for x in self.qt_vars:
  741. y=x.replace('Qt' + qt_ver, 'Qt')[2:].upper()
  742. self.env.append_unique('DEFINES_%s' % x.upper(), 'QT_%s_LIB' % y)
  743. def options(opt):
  744. """
  745. Command-line options
  746. """
  747. opt.add_option('--want-rpath', action='store_true', default=False, dest='want_rpath', help='enable the rpath for qt libraries')
  748. for i in 'qtdir qtbin qtlibs'.split():
  749. opt.add_option('--'+i, type=str, default='', dest=i)
  750. opt.add_option('--translate', action='store_true', help='collect translation strings', dest='trans_qt5', default=False)
  751. opt.add_option('--qtextralibs', type=str, default='', dest='qtextralibs', help='additional qt libraries on the system to add to default ones, comma separated')