From a6ecc6b289962d1b7ce26a7e277a585adb47f15d Mon Sep 17 00:00:00 2001 From: Pierre Lamot Date: Wed, 12 Jun 2024 12:03:24 +0200 Subject: [PATCH] qt module: allow has_tools to specify which tools to check This allows checking for tools that may not be available in older version of qt or avoiding requesting tools that may not be necessary for a given project Co-authored-by: Nirbheek Chauhan --- docs/markdown/Qt6-module.md | 3 ++ docs/markdown/snippets/qt_has_tools_ignore.md | 12 ++++++ mesonbuild/modules/_qt.py | 27 ++++++++++--- test cases/frameworks/4 qt/meson.build | 38 +++++++++++-------- test cases/frameworks/4 qt/meson_options.txt | 1 + unittests/linuxliketests.py | 14 +++++++ 6 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 docs/markdown/snippets/qt_has_tools_ignore.md diff --git a/docs/markdown/Qt6-module.md b/docs/markdown/Qt6-module.md index 0a453dd41443..8b6d2ebc87b1 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 000000000000..4def48db2b51 --- /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 ebb8a3994097..9f10c58266a5 100644 --- a/mesonbuild/modules/_qt.py +++ b/mesonbuild/modules/_qt.py @@ -27,6 +27,7 @@ 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 @@ class PreprocessKwArgs(TypedDict): class HasToolKwArgs(kwargs.ExtractRequired): method: str + tools: T.List[Literal['moc', 'uic', 'rcc', 'lrelease']] class CompileTranslationsKwArgs(TypedDict): @@ -91,10 +93,21 @@ class CompileTranslationsKwArgs(TypedDict): 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 @@ def __init__(self, interpreter: 'Interpreter', qt_version: int = 5): # 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 @@ def _parse_qrc_deps(self, state: 'ModuleState', '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 @@ def has_tools(self, state: 'ModuleState', args: T.Tuple, kwargs: 'HasToolKwArgs' 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 54cd7cb9bac3..58ec4d16f786 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 223f4fb0b05f..9a7513434012 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 f92c992cc613..bf0437bca582 100644 --- a/unittests/linuxliketests.py +++ b/unittests/linuxliketests.py @@ -36,6 +36,7 @@ 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 @@ def test_generate_gir_with_address_sanitizer(self): 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