3 # Thomas Nagy, 2006-2018 (ita)
6 This tool helps with finding Qt5 tools and libraries,
7 and also provides syntactic sugar for using Qt5 tools.
9 The following snippet illustrates the tool usage::
12 opt.load('compiler_cxx qt5')
15 conf.load('compiler_cxx qt5')
19 features = 'qt5 cxx cxxprogram',
20 uselib = 'QT5CORE QT5GUI QT5OPENGL QT5SVG',
21 source = 'main.cpp textures.qrc aboutDialog.ui',
25 Here, the UI description and resource files will be processed
33 You also need to edit your sources accordingly:
35 - the normal way of doing things is to have your C++ files
36 include the .moc file.
37 This is regarded as the best practice (and provides much faster
39 It also implies that the include paths have beenset properly.
41 - to have the include paths added automatically, use the following::
43 from waflib.TaskGen import feature, before_method, after_method
45 @after_method('process_source')
46 @before_method('apply_incpaths')
47 def add_includes_paths(self):
48 incs = set(self.to_list(getattr(self, 'includes', '')))
49 for x in self.compiled_tasks:
50 incs.add(x.inputs[0].parent.path_from(self.path))
51 self.includes = sorted(incs)
53 Note: another tool provides Qt processing that does not require
54 .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.
60 The detection uses pkg-config on Linux by default. To force static library detection use:
61 QT5_XCOMPILE=1 QT5_FORCE_STATIC=1 waf configure
64 from __future__ import with_statement
67 from xml.sax import make_parser
68 from xml.sax.handler import ContentHandler
71 ContentHandler = object
76 from waflib.Tools import cxx
77 from waflib import Task, Utils, Options, Errors, Context
78 from waflib.TaskGen import feature, after_method, extension, before_method
79 from waflib.Configure import conf
80 from waflib import Logs
82 MOC_H = ['.h', '.hpp', '.hxx', '.hh']
84 File extensions associated to .moc files
89 File extension for the resource (.qrc) files
94 File extension for the user interface (.ui) files
97 EXT_QT5 = ['.cpp', '.cc', '.cxx', '.C']
99 File extensions of C++ files that may require a .moc processing
102 class qxx(Task.classes['cxx']):
104 Each C++ file can have zero or several .moc files to create.
105 They are known only when the files are scanned (preprocessor)
106 To avoid scanning the c++ files each time (parsing C/C++), the results
107 are retrieved from the task cache (bld.node_deps/bld.raw_deps).
108 The moc tasks are also created *dynamically* during the build.
111 def __init__(self, *k, **kw):
112 Task.Task.__init__(self, *k, **kw)
115 def runnable_status(self):
117 Compute the task signature to make sure the scanner was executed. Create the
118 moc tasks by using :py:meth:`waflib.Tools.qt5.qxx.add_moc_tasks` (if necessary),
119 then postpone the task execution (there is no need to recompute the task signature).
122 return Task.Task.runnable_status(self)
124 for t in self.run_after:
126 return Task.ASK_LATER
128 return Task.Task.runnable_status(self)
130 def create_moc_task(self, h_node, m_node):
132 If several libraries use the same classes, it is possible that moc will run several times (Issue 1318)
133 It is not possible to change the file names, but we can assume that the moc transformation will be identical,
134 and the moc tasks can be shared in a global cache.
137 moc_cache = self.generator.bld.moc_cache
138 except AttributeError:
139 moc_cache = self.generator.bld.moc_cache = {}
142 return moc_cache[h_node]
144 tsk = moc_cache[h_node] = Task.classes['moc'](env=self.env, generator=self.generator)
145 tsk.set_inputs(h_node)
146 tsk.set_outputs(m_node)
147 tsk.env.append_unique('MOC_FLAGS', '-i')
150 self.generator.tasks.append(tsk)
152 # direct injection in the build phase (safe because called from the main thread)
153 gen = self.generator.bld.producer
154 gen.outstanding.append(tsk)
160 # remove the signature, it must be recomputed with the moc task
161 delattr(self, 'cache_sig')
163 def add_moc_tasks(self):
165 Creates moc tasks by looking in the list of file dependencies ``bld.raw_deps[self.uid()]``
167 node = self.inputs[0]
168 bld = self.generator.bld
171 # compute the signature once to know if there is a moc file to create
174 # the moc file may be referenced somewhere else
177 # remove the signature, it must be recomputed with the moc task
178 delattr(self, 'cache_sig')
180 include_nodes = [node.parent] + self.generator.includes_nodes
184 for d in bld.raw_deps.get(self.uid(), []):
185 if not d.endswith('.moc'):
188 # process that base.moc only once
193 # find the source associated with the moc file
197 # foo.moc from foo.cpp
198 prefix = node.name[:node.name.rfind('.')]
202 # this deviates from the standard
203 # if bar.cpp includes foo.moc, then assume it is from foo.h
204 for x in include_nodes:
206 h_node = x.find_node(base2 + e)
213 m_node = h_node.change_ext('.moc')
215 raise Errors.WafError('No source found for %r which is a moc file' % d)
217 # create the moc task
218 task = self.create_moc_task(h_node, m_node)
219 moctasks.append(task)
221 # simple scheduler dependency: run the moc task before others
222 self.run_after.update(set(moctasks))
225 class trans_update(Task.Task):
226 """Updates a .ts files from a list of C++ files"""
227 run_str = '${QT_LUPDATE} ${SRC} -ts ${TGT}'
230 class XMLHandler(ContentHandler):
232 Parses ``.qrc`` files
235 ContentHandler.__init__(self)
238 def startElement(self, name, attrs):
241 def endElement(self, name):
243 self.files.append(str(''.join(self.buf)))
244 def characters(self, cars):
245 self.buf.append(cars)
248 def create_rcc_task(self, node):
249 "Creates rcc and cxx tasks for ``.qrc`` files"
250 rcnode = node.change_ext('_rc.%d.cpp' % self.idx)
251 self.create_task('rcc', node, rcnode)
252 cpptask = self.create_task('cxx', rcnode, rcnode.change_ext('.o'))
254 self.compiled_tasks.append(cpptask)
255 except AttributeError:
256 self.compiled_tasks = [cpptask]
260 def create_uic_task(self, node):
261 "Create uic tasks for user interface ``.ui`` definition files"
264 If UIC file is used in more than one bld, we would have a conflict in parallel execution
265 It is not possible to change the file names (like .self.idx. as for objects) as they have
266 to be referenced by the source file, but we can assume that the transformation will be identical
267 and the tasks can be shared in a global cache.
270 uic_cache = self.bld.uic_cache
271 except AttributeError:
272 uic_cache = self.bld.uic_cache = {}
274 if node not in uic_cache:
275 uictask = uic_cache[node] = self.create_task('ui5', node)
276 uictask.outputs = [node.parent.find_or_declare(self.env.ui_PATTERN % node.name[:-3])]
279 def add_lang(self, node):
280 """Adds all the .ts file into ``self.lang``"""
281 self.lang = self.to_list(getattr(self, 'lang', [])) + [node]
284 @before_method('process_source')
285 def process_mocs(self):
287 Processes MOC files included in headers::
290 bld.program(features='qt5', source='main.cpp', target='app', use='QT5CORE', moc='foo.h')
292 The build will run moc on foo.h to create moc_foo.n.cpp. The number in the file name
293 is provided to avoid name clashes when the same headers are used by several targets.
295 lst = self.to_nodes(getattr(self, 'moc', []))
296 self.source = self.to_list(getattr(self, 'source', []))
298 prefix = x.name[:x.name.rfind('.')] # foo.h -> foo
299 moc_target = 'moc_%s.%d.cpp' % (prefix, self.idx)
300 moc_node = x.parent.find_or_declare(moc_target)
301 self.source.append(moc_node)
303 self.create_task('moc', x, moc_node)
306 @after_method('apply_link')
309 Adds MOC_FLAGS which may be necessary for moc::
312 bld.program(features='qt5', source='main.cpp', target='app', use='QT5CORE')
314 The additional parameters are:
316 :param lang: list of translation files (\*.ts) to process
317 :type lang: list of :py:class:`waflib.Node.Node` or string without the .ts extension
318 :param update: whether to process the C++ files to update the \*.ts files (use **waf --translate**)
320 :param langname: if given, transform the \*.ts files into a .qrc files to include in the binary file
321 :type langname: :py:class:`waflib.Node.Node` or string without the .qrc extension
323 if getattr(self, 'lang', None):
325 for x in self.to_list(self.lang):
326 if isinstance(x, str):
327 x = self.path.find_resource(x + '.ts')
328 qmtasks.append(self.create_task('ts2qm', x, x.change_ext('.%d.qm' % self.idx)))
330 if getattr(self, 'update', None) and Options.options.trans_qt5:
331 cxxnodes = [a.inputs[0] for a in self.compiled_tasks] + [
332 a.inputs[0] for a in self.tasks if a.inputs and a.inputs[0].name.endswith('.ui')]
334 self.create_task('trans_update', cxxnodes, x.inputs)
336 if getattr(self, 'langname', None):
337 qmnodes = [x.outputs[0] for x in qmtasks]
338 rcnode = self.langname
339 if isinstance(rcnode, str):
340 rcnode = self.path.find_or_declare(rcnode + ('.%d.qrc' % self.idx))
341 t = self.create_task('qm2rcc', qmnodes, rcnode)
342 k = create_rcc_task(self, t.outputs[0])
343 self.link_task.inputs.append(k.outputs[0])
346 for flag in self.to_list(self.env.CXXFLAGS):
350 if f in ('-D', '-I', '/D', '/I'):
352 lst.append('-' + flag[1:])
355 self.env.append_value('MOC_FLAGS', lst)
358 def cxx_hook(self, node):
360 Re-maps C++ file extensions to the :py:class:`waflib.Tools.qt5.qxx` task.
362 return self.create_compiled_task('qxx', node)
364 class rcc(Task.Task):
366 Processes ``.qrc`` files
369 run_str = '${QT_RCC} -name ${tsk.rcname()} ${SRC[0].abspath()} ${RCC_ST} -o ${TGT}'
373 return os.path.splitext(self.inputs[0].name)[0]
376 """Parse the *.qrc* files"""
378 Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!')
381 parser = make_parser()
382 curHandler = XMLHandler()
383 parser.setContentHandler(curHandler)
384 with open(self.inputs[0].abspath(), 'r') as f:
389 root = self.inputs[0].parent
390 for x in curHandler.files:
391 nd = root.find_resource(x)
396 return (nodes, names)
398 def quote_flag(self, x):
400 Override Task.quote_flag. QT parses the argument files
401 differently than cl.exe and link.exe
411 class moc(Task.Task):
413 Creates ``.moc`` files
416 run_str = '${QT_MOC} ${MOC_FLAGS} ${MOCCPPPATH_ST:INCPATHS} ${MOCDEFINES_ST:DEFINES} ${SRC} ${MOC_ST} ${TGT}'
418 def quote_flag(self, x):
420 Override Task.quote_flag. QT parses the argument files
421 differently than cl.exe and link.exe
431 class ui5(Task.Task):
433 Processes ``.ui`` files
436 run_str = '${QT_UIC} ${SRC} -o ${TGT}'
439 class ts2qm(Task.Task):
441 Generates ``.qm`` files from ``.ts`` files
444 run_str = '${QT_LRELEASE} ${QT_LRELEASE_FLAGS} ${SRC} -qm ${TGT}'
446 class qm2rcc(Task.Task):
448 Generates ``.qrc`` files from ``.qm`` files
453 """Create a qrc file including the inputs"""
454 txt = '\n'.join(['<file>%s</file>' % k.path_from(self.outputs[0].parent) for k in self.inputs])
455 code = '<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n%s\n</qresource>\n</RCC>' % txt
456 self.outputs[0].write(code)
460 Besides the configuration options, the environment variable QT5_ROOT may be used
461 to give the location of the qt5 libraries (absolute path).
463 The detection uses the program ``pkg-config`` through :py:func:`waflib.Tools.config_c.check_cfg`
465 self.find_qt5_binaries()
466 self.set_qt5_libs_dir()
467 self.set_qt5_libs_to_check()
468 self.set_qt5_defines()
469 self.find_qt5_libraries()
471 self.simplify_qt5_libs()
473 # warn about this during the configuration too
475 Logs.error('No xml.sax support was found, rcc dependencies will be incomplete!')
477 if 'COMPILER_CXX' not in self.env:
478 self.fatal('No CXX compiler defined: did you forget to configure compiler_cxx first?')
480 # Qt5 may be compiled with '-reduce-relocations' which requires dependent programs to have -fPIE or -fPIC?
481 frag = '#include <QApplication>\nint main(int argc, char **argv) {return 0;}\n'
482 uses = 'QT5CORE QT5WIDGETS QT5GUI'
483 for flag in [[], '-fPIE', '-fPIC', '-std=c++11' , ['-std=c++11', '-fPIE'], ['-std=c++11', '-fPIC']]:
484 msg = 'See if Qt files compile '
486 msg += 'with %s' % flag
488 self.check(features='qt5 cxx', use=uses, uselib_store='qt5', cxxflags=flag, fragment=frag, msg=msg)
489 except self.errors.ConfigurationError:
494 self.fatal('Could not build a simple Qt application')
496 # FreeBSD does not add /usr/local/lib and the pkg-config files do not provide it either :-/
497 if Utils.unversioned_sys_platform() == 'freebsd':
498 frag = '#include <QApplication>\nint main(int argc, char **argv) { QApplication app(argc, argv); return NULL != (void*) (&app);}\n'
500 self.check(features='qt5 cxx cxxprogram', use=uses, fragment=frag, msg='Can we link Qt programs on FreeBSD directly?')
501 except self.errors.ConfigurationError:
502 self.check(features='qt5 cxx cxxprogram', use=uses, uselib_store='qt5', libpath='/usr/local/lib', fragment=frag, msg='Is /usr/local/lib required?')
505 def find_qt5_binaries(self):
507 Detects Qt programs such as qmake, moc, uic, lrelease
510 opt = Options.options
512 qtdir = getattr(opt, 'qtdir', '')
513 qtbin = getattr(opt, 'qtbin', '')
518 qtbin = os.path.join(qtdir, 'bin')
520 # the qt directory has been given from QT5_ROOT - deduce the qt binary path
522 qtdir = self.environ.get('QT5_ROOT', '')
523 qtbin = self.environ.get('QT5_BIN') or os.path.join(qtdir, 'bin')
528 # no qtdir, look in the path and in /usr/local/Trolltech
530 paths = self.environ.get('PATH', '').split(os.pathsep)
531 paths.extend(['/usr/share/qt5/bin', '/usr/local/lib/qt5/bin'])
533 lst = Utils.listdir('/usr/local/Trolltech/')
541 # keep the highest version
542 qtdir = '/usr/local/Trolltech/%s/' % lst[0]
543 qtbin = os.path.join(qtdir, 'bin')
546 # at the end, try to find qmake in the paths given
547 # keep the one with the highest version
549 prev_ver = ['5', '0', '0']
550 for qmk in ('qmake-qt5', 'qmake5', 'qmake'):
552 qmake = self.find_program(qmk, path_list=paths)
553 except self.errors.ConfigurationError:
557 version = self.cmd_and_log(qmake + ['-query', 'QT_VERSION']).strip()
558 except self.errors.WafError:
562 new_ver = version.split('.')
563 if new_ver > prev_ver:
567 # qmake could not be found easily, rely on qtchooser
570 self.find_program('qtchooser')
571 except self.errors.ConfigurationError:
574 cmd = self.env.QTCHOOSER + ['-qt=5', '-run-tool=qmake']
576 version = self.cmd_and_log(cmd + ['-query', 'QT_VERSION'])
577 except self.errors.WafError:
583 self.env.QMAKE = cand
585 self.fatal('Could not find qmake for qt5')
587 self.env.QT_HOST_BINS = qtbin = self.cmd_and_log(self.env.QMAKE + ['-query', 'QT_HOST_BINS']).strip()
588 paths.insert(0, qtbin)
590 def find_bin(lst, var):
595 ret = self.find_program(f, path_list=paths)
596 except self.errors.ConfigurationError:
602 find_bin(['uic-qt5', 'uic'], 'QT_UIC')
604 self.fatal('cannot find the uic compiler for qt5')
606 self.start_msg('Checking for uic version')
607 uicver = self.cmd_and_log(env.QT_UIC + ['-version'], output=Context.BOTH)
608 uicver = ''.join(uicver).strip()
609 uicver = uicver.replace('Qt User Interface Compiler ','').replace('User Interface Compiler for Qt', '')
611 if uicver.find(' 3.') != -1 or uicver.find(' 4.') != -1:
612 self.fatal('this uic compiler is for qt3 or qt4, add uic for qt5 to your path')
614 find_bin(['moc-qt5', 'moc'], 'QT_MOC')
615 find_bin(['rcc-qt5', 'rcc'], 'QT_RCC')
616 find_bin(['lrelease-qt5', 'lrelease'], 'QT_LRELEASE')
617 find_bin(['lupdate-qt5', 'lupdate'], 'QT_LUPDATE')
619 env.UIC_ST = '%s -o %s'
621 env.ui_PATTERN = 'ui_%s.h'
622 env.QT_LRELEASE_FLAGS = ['-silent']
623 env.MOCCPPPATH_ST = '-I%s'
624 env.MOCDEFINES_ST = '-D%s'
627 def set_qt5_libs_dir(self):
629 qtlibs = getattr(Options.options, 'qtlibs', None) or self.environ.get('QT5_LIBDIR')
632 qtlibs = self.cmd_and_log(env.QMAKE + ['-query', 'QT_INSTALL_LIBS']).strip()
633 except Errors.WafError:
634 qtdir = self.cmd_and_log(env.QMAKE + ['-query', 'QT_INSTALL_PREFIX']).strip()
635 qtlibs = os.path.join(qtdir, 'lib')
636 self.msg('Found the Qt5 libraries in', qtlibs)
640 def find_single_qt5_lib(self, name, uselib, qtlibs, qtincludes, force_static):
643 exts = ('.a', '.lib')
646 exts = ('.so', '.lib')
651 for k in ('', '5') if Utils.is_win32 else ['']:
652 for p in ('lib', ''):
653 yield (p, name, k, x)
655 for tup in lib_names():
657 path = os.path.join(qtlibs, k)
658 if os.path.exists(path):
659 if env.DEST_OS == 'win32':
660 libval = ''.join(tup[:-1])
663 env.append_unique(prefix + '_' + uselib, libval)
664 env.append_unique('%sPATH_%s' % (prefix, uselib), qtlibs)
665 env.append_unique('INCLUDES_' + uselib, qtincludes)
666 env.append_unique('INCLUDES_' + uselib, os.path.join(qtincludes, name.replace('Qt5', 'Qt')))
671 def find_qt5_libraries(self):
674 qtincludes = self.environ.get('QT5_INCLUDES') or self.cmd_and_log(env.QMAKE + ['-query', 'QT_INSTALL_HEADERS']).strip()
675 force_static = self.environ.get('QT5_FORCE_STATIC')
677 if self.environ.get('QT5_XCOMPILE'):
678 self.fatal('QT5_XCOMPILE Disables pkg-config detection')
679 self.check_cfg(atleast_pkgconfig_version='0.1')
680 except self.errors.ConfigurationError:
681 for i in self.qt5_vars:
683 if Utils.unversioned_sys_platform() == 'darwin':
684 # Since at least qt 4.7.3 each library locates in separate directory
685 fwk = i.replace('Qt5', 'Qt')
686 frameworkName = fwk + '.framework'
688 qtDynamicLib = os.path.join(env.QTLIBS, frameworkName, fwk)
689 if os.path.exists(qtDynamicLib):
690 env.append_unique('FRAMEWORK_' + uselib, fwk)
691 env.append_unique('FRAMEWORKPATH_' + uselib, env.QTLIBS)
692 self.msg('Checking for %s' % i, qtDynamicLib, 'GREEN')
694 self.msg('Checking for %s' % i, False, 'YELLOW')
695 env.append_unique('INCLUDES_' + uselib, os.path.join(env.QTLIBS, frameworkName, 'Headers'))
697 ret = self.find_single_qt5_lib(i, uselib, env.QTLIBS, qtincludes, force_static)
698 if not force_static and not ret:
699 ret = self.find_single_qt5_lib(i, uselib, env.QTLIBS, qtincludes, True)
700 self.msg('Checking for %s' % i, ret, 'GREEN' if ret else 'YELLOW')
702 path = '%s:%s:%s/pkgconfig:/usr/lib/qt5/lib/pkgconfig:/opt/qt5/lib/pkgconfig:/usr/lib/qt5/lib:/opt/qt5/lib' % (
703 self.environ.get('PKG_CONFIG_PATH', ''), env.QTLIBS, env.QTLIBS)
704 for i in self.qt5_vars:
705 self.check_cfg(package=i, args='--cflags --libs', mandatory=False, force_static=force_static, pkg_config_path=path)
708 def simplify_qt5_libs(self):
710 Since library paths make really long command-lines,
711 and since everything depends on qtcore, remove the qtcore ones from qtgui, etc
714 def process_lib(vars_, coreval):
720 value = env['LIBPATH_'+var]
728 env['LIBPATH_'+var] = accu
729 process_lib(self.qt5_vars, 'LIBPATH_QTCORE')
732 def add_qt5_rpath(self):
734 Defines rpath entries for Qt libraries
737 if getattr(Options.options, 'want_rpath', False):
738 def process_rpath(vars_, coreval):
741 value = env['LIBPATH_' + var]
749 accu.append('-Wl,--rpath='+lib)
750 env['RPATH_' + var] = accu
751 process_rpath(self.qt5_vars, 'LIBPATH_QTCORE')
754 def set_qt5_libs_to_check(self):
755 self.qt5_vars = Utils.to_list(getattr(self, 'qt5_vars', []))
756 if not self.qt5_vars:
757 dirlst = Utils.listdir(self.env.QTLIBS)
759 pat = self.env.cxxshlib_PATTERN
761 pat = pat.replace('.dll', '.lib')
762 if self.environ.get('QT5_FORCE_STATIC'):
763 pat = self.env.cxxstlib_PATTERN
764 if Utils.unversioned_sys_platform() == 'darwin':
765 pat = "%s\.framework"
766 re_qt = re.compile(pat%'Qt5?(?P<name>.*)'+'$')
770 self.qt5_vars.append("Qt5%s" % m.group('name'))
771 if not self.qt5_vars:
772 self.fatal('cannot find any Qt5 library (%r)' % self.env.QTLIBS)
774 qtextralibs = getattr(Options.options, 'qtextralibs', None)
776 self.qt5_vars.extend(qtextralibs.split(','))
779 def set_qt5_defines(self):
780 if sys.platform != 'win32':
782 for x in self.qt5_vars:
783 y=x.replace('Qt5', 'Qt')[2:].upper()
784 self.env.append_unique('DEFINES_%s' % x.upper(), 'QT_%s_LIB' % y)
790 opt.add_option('--want-rpath', action='store_true', default=False, dest='want_rpath', help='enable the rpath for qt libraries')
791 for i in 'qtdir qtbin qtlibs'.split():
792 opt.add_option('--'+i, type='string', default='', dest=i)
794 opt.add_option('--translate', action='store_true', help='collect translation strings', dest='trans_qt5', default=False)
795 opt.add_option('--qtextralibs', type='string', default='', dest='qtextralibs', help='additional qt libraries on the system to add to default ones, comma separated')