diff --git a/docs/markdown/Qt6-module.md b/docs/markdown/Qt6-module.md index 673b49996..7b6f94715 100644 --- a/docs/markdown/Qt6-module.md +++ b/docs/markdown/Qt6-module.md @@ -156,6 +156,9 @@ This method takes the following keyword arguments: `true` or an enabled [`feature`](Build-options.md#features) and some tools are missing Meson will abort. - `method` string: The method to use to detect Qt, see [[dependency]] +- `tools`: string[]: *Since 1.6.0*. List of tools to check. Testable tools + are `moc`, `uic`, `rcc` and `lrelease`. By default `tools` is set to `['moc', + 'uic', 'rcc', 'lrelease']` ## Dependencies diff --git a/docs/markdown/snippets/qt_has_tools_ignore.md b/docs/markdown/snippets/qt_has_tools_ignore.md new file mode 100644 index 000000000..4def48db2 --- /dev/null +++ b/docs/markdown/snippets/qt_has_tools_ignore.md @@ -0,0 +1,12 @@ +## Tools can be selected when calling `has_tools()` on the Qt modules + +When checking for the presence of Qt tools, you can now explictly ask Meson +which tools you need. This is particularly useful when you do not need +`lrelease` because you are not shipping any translations. For example: + +```meson +qt6_mod = import('qt6') +qt6_mod.has_tools(required: true, tools: ['moc', 'uic', 'rcc']) +``` + +valid tools are `moc`, `uic`, `rcc` and `lrelease`. diff --git a/mesonbuild/modules/_qt.py b/mesonbuild/modules/_qt.py index ebb8a3994..9f10c5826 100644 --- a/mesonbuild/modules/_qt.py +++ b/mesonbuild/modules/_qt.py @@ -27,6 +27,7 @@ if T.TYPE_CHECKING: from ..interpreter import kwargs from ..mesonlib import FileOrString from ..programs import ExternalProgram + from typing_extensions import Literal QtDependencyType = T.Union[QtPkgConfigDependency, QmakeQtDependency] @@ -80,6 +81,7 @@ if T.TYPE_CHECKING: class HasToolKwArgs(kwargs.ExtractRequired): method: str + tools: T.List[Literal['moc', 'uic', 'rcc', 'lrelease']] class CompileTranslationsKwArgs(TypedDict): @@ -91,10 +93,21 @@ if T.TYPE_CHECKING: rcc_extra_arguments: T.List[str] ts_files: T.List[T.Union[str, File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]] +def _list_in_set_validator(choices: T.Set[str]) -> T.Callable[[T.List[str]], T.Optional[str]]: + """Check that the choice given was one of the given set.""" + def inner(checklist: T.List[str]) -> T.Optional[str]: + invalid = set(checklist).difference(choices) + if invalid: + return f"invalid selections {', '.join(sorted(invalid))}, valid elements are {', '.join(sorted(choices))}." + return None + + return inner + class QtBaseModule(ExtensionModule): _tools_detected = False _rcc_supports_depfiles = False _moc_supports_depfiles = False + _set_of_qt_tools = {'moc', 'uic', 'rcc', 'lrelease'} def __init__(self, interpreter: 'Interpreter', qt_version: int = 5): ExtensionModule.__init__(self, interpreter) @@ -102,10 +115,7 @@ class QtBaseModule(ExtensionModule): # It is important that this list does not change order as the order of # the returned ExternalPrograms will change as well self.tools: T.Dict[str, T.Union[ExternalProgram, build.Executable]] = { - 'moc': NonExistingExternalProgram('moc'), - 'uic': NonExistingExternalProgram('uic'), - 'rcc': NonExistingExternalProgram('rcc'), - 'lrelease': NonExistingExternalProgram('lrelease'), + tool: NonExistingExternalProgram(tool) for tool in self._set_of_qt_tools } self.methods.update({ 'has_tools': self.has_tools, @@ -258,6 +268,10 @@ class QtBaseModule(ExtensionModule): 'qt.has_tools', KwargInfo('required', (bool, options.UserFeatureOption), default=False), KwargInfo('method', str, default='auto'), + KwargInfo('tools', ContainerTypeInfo(list, str), listify=True, + default=['moc', 'uic', 'rcc', 'lrelease'], + validator=_list_in_set_validator(_set_of_qt_tools), + since='1.6.0'), ) def has_tools(self, state: 'ModuleState', args: T.Tuple, kwargs: 'HasToolKwArgs') -> bool: method = kwargs.get('method', 'auto') @@ -269,8 +283,9 @@ class QtBaseModule(ExtensionModule): mlog.log('qt.has_tools skipped: feature', mlog.bold(feature), 'disabled') return False self._detect_tools(state, method, required=False) - for tool in self.tools.values(): - if not tool.found(): + for tool in kwargs['tools']: + assert tool in self._set_of_qt_tools, f'tools must be in {self._set_of_qt_tools}' + if not self.tools[tool].found(): if required: raise MesonException('Qt tools not found') return False diff --git a/test cases/frameworks/4 qt/meson.build b/test cases/frameworks/4 qt/meson.build index 54cd7cb9b..58ec4d16f 100644 --- a/test cases/frameworks/4 qt/meson.build +++ b/test cases/frameworks/4 qt/meson.build @@ -48,7 +48,13 @@ foreach qt : ['qt4', 'qt5', 'qt6'] qtdep = dependency(qt, modules : qt_modules, main : true, private_headers: true, required : required, method : get_option('method')) if qtdep.found() qtmodule = import(qt) - assert(qtmodule.has_tools(), 'You may be missing a devel package. (qttools5-dev-tools on Debian based systems)') + if get_option('expect_lrelease') + assert(qtmodule.has_tools(), 'You may be missing a devel package. (qttools5-dev-tools on Debian based systems)') + else + assert(not qtmodule.has_tools(), 'Unexpectedly found lrelease') + assert(not qtmodule.has_tools(tools: ['lrelease']), 'Unexpectedly found lrelease') + assert(qtmodule.has_tools(tools: ['moc', 'uic', 'rcc']), 'You may be missing a devel package. (qttools5-dev-tools on Debian based systems)') + endif # Test that fetching a variable works and yields a non-empty value assert(qtdep.get_variable('prefix', configtool: 'QT_INSTALL_PREFIX') != '') @@ -91,23 +97,25 @@ foreach qt : ['qt4', 'qt5', 'qt6'] # qt4-rcc and qt5-rcc take different arguments, for example qt4: ['-compress', '3']; qt5: '--compress=3' qtmodule.preprocess(qt + 'testrccarg', qresources : files(['stuff.qrc', 'stuff2.qrc']), rcc_extra_arguments : '--compress=3', method : get_option('method')) - translations_cpp = qtmodule.compile_translations(qresource: qt+'_lang.qrc') - # unity builds suck and definitely cannot handle two qrc embeds in one compilation unit - unityproof_translations = static_library(qt+'unityproof_translations', translations_cpp, dependencies: qtdep) + if get_option('expect_lrelease') + translations_cpp = qtmodule.compile_translations(qresource: qt+'_lang.qrc') + # unity builds suck and definitely cannot handle two qrc embeds in one compilation unit + unityproof_translations = static_library(qt+'unityproof_translations', translations_cpp, dependencies: qtdep) - extra_cpp_args += '-DQT="@0@"'.format(qt) - qexe = executable(qt + 'app', - sources : ['main.cpp', 'mainWindow.cpp', # Sources that don't need preprocessing. - prep, prep_rcc], - dependencies : qtdep, - link_with: unityproof_translations, - cpp_args: extra_cpp_args, - gui_app : true) + extra_cpp_args += '-DQT="@0@"'.format(qt) + qexe = executable(qt + 'app', + sources : ['main.cpp', 'mainWindow.cpp', # Sources that don't need preprocessing. + prep, prep_rcc], + dependencies : qtdep, + link_with: unityproof_translations, + cpp_args: extra_cpp_args, + gui_app : true) - # We need a console test application because some test environments - # do not have an X server. + # We need a console test application because some test environments + # do not have an X server. - translations = qtmodule.compile_translations(ts_files : qt+'core_fr.ts', build_by_default : true) + translations = qtmodule.compile_translations(ts_files : qt+'core_fr.ts', build_by_default : true) + endif qtcore = dependency(qt, modules : 'Core', method : get_option('method')) diff --git a/test cases/frameworks/4 qt/meson_options.txt b/test cases/frameworks/4 qt/meson_options.txt index 223f4fb0b..9a7513434 100644 --- a/test cases/frameworks/4 qt/meson_options.txt +++ b/test cases/frameworks/4 qt/meson_options.txt @@ -1,2 +1,3 @@ option('method', type : 'string', value : 'auto', description : 'The method to use to find Qt') option('required', type : 'string', value : 'qt5', description : 'The version of Qt which is required to be present') +option('expect_lrelease', type: 'boolean', value: true) diff --git a/unittests/linuxliketests.py b/unittests/linuxliketests.py index f92c992cc..bf0437bca 100644 --- a/unittests/linuxliketests.py +++ b/unittests/linuxliketests.py @@ -36,6 +36,7 @@ from mesonbuild.compilers.cpp import AppleClangCPPCompiler from mesonbuild.compilers.objc import AppleClangObjCCompiler from mesonbuild.compilers.objcpp import AppleClangObjCPPCompiler from mesonbuild.dependencies.pkgconfig import PkgConfigDependency, PkgConfigCLI, PkgConfigInterface +from mesonbuild.programs import NonExistingExternalProgram import mesonbuild.modules.pkgconfig PKG_CONFIG = os.environ.get('PKG_CONFIG', 'pkg-config') @@ -317,6 +318,19 @@ class LinuxlikeTests(BasePlatformTests): self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false']) self.build() + def test_qt5dependency_no_lrelease(self): + ''' + Test that qt5 detection with qmake works. This can't be an ordinary + test case because it involves setting the environment. + ''' + testdir = os.path.join(self.framework_test_dir, '4 qt') + def _no_lrelease(self, prog, *args, **kwargs): + if 'lrelease' in prog: + return NonExistingExternalProgram(prog) + return self._interpreter.find_program_impl(prog, *args, **kwargs) + with mock.patch.object(mesonbuild.modules.ModuleState, 'find_program', _no_lrelease): + self.init(testdir, inprocess=True, extra_args=['-Dmethod=qmake', '-Dexpect_lrelease=false']) + def test_qt5dependency_qmake_detection(self): ''' Test that qt5 detection with qmake works. This can't be an ordinary