CMake installation of QML Module fail
-
Hello everyone !
I'm developing an application in QML with a C++ back-end.
The project is based on CMake ≥ 3.21 and Qt-6.6.1I try to make a package (but first at least an installation folder) for my app, with all the necessary dependencies.
I'm using the qt cmake macros mostly introduced in qt 6.3.
For this post, I renamed the project as "MyProject", and the integrated QML module "MyProject_Module".CMakeFile.txt (simplified):
cmake_minimum_required(VERSION 3.21) project(MyProject VERSION 0.0.0 LANGUAGES CXX ) qt_standard_project_setup() set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 6.6 REQUIRED COMPONENTS Core Quick Qml Svg Widgets #Needed for MessageDialog ) set(SRC_PATH ${CMAKE_CURRENT_LIST_DIR}/sources/common) qt_add_executable(${PROJECT_NAME} ${SRC_PATH}/main.cpp ${SRC_PATH}/StdAfx.h ${CMAKE_CURRENT_LIST_DIR}/README.md ${CMAKE_CURRENT_LIST_DIR}/.gitignore ) qt_add_qml_module(${PROJECT_NAME} URI ${PROJECT_NAME}_Module VERSION 1.0 RESOURCE_PREFIX ":/qt/qml/" QML_FILES qml/DM_MainWindow.qml #My QML files qml/DM_DeviceListSelector.qml SOURCES ${SRC_PATH}/DM_Device.cpp #Some QML elements in C++ ${SRC_PATH}/DM_Device.h ) target_include_directories(${PROJECT_NAME} PRIVATE ${SRC_PATH} ) target_link_libraries(${PROJECT_NAME} PUBLIC Qt6::Core Qt6::Quick Qt6::Qml Qt6::Svg Qt6::Widgets #Needed for MessageDialog ) set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/_install") install(TARGETS ${PROJECT_NAME} BUNDLE DESTINATION . LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) qt_generate_deploy_qml_app_script( TARGET ${PROJECT_NAME} OUTPUT_SCRIPT deploy_qml_script NO_TRANSLATIONS ) install(SCRIPT ${deploy_qml_script})
When I run cmake --target install command, it install most of the dependences I want, but not the MyProject_Module.
Here is the result target tree :
_install ├── bin ├── MyProject.exe ├── All assets I requested to copy ├── qt.conf └── Qt .dlls installed by windeployqt (Qt6Core, Qt6Gui, opengl32sw...) ├── plugin ├── generic ├── iconengines ├── imageformats ├── networkinformation ├── platforminputcontexts ├── platforms ├── qmltooling ├── styles └── tls ├── qml ├── Qt (->qmlDir + dll from Qt modules) ├── QtQml (->qmlDir + dll from QtQml modules) └── QtQuick (->qmlDir + dll from QtQuick modules)
My "MyPoject_Module" folder containing the qmlDir should have been copied into the _install/qml folder next to Qt's modules, but it's not and I don't manage to understand why...
When I copy this folder here by hand, the app can be launched.I've edited the script generated by qt_generate_deploy_qml_app_script (and force to use it in install) to have more debug informations.
<build-dir>/.qt/deploy_qml_app_MyProject_<some numbers>.cmake (modified for debug) :
include(<build-dir>/.qt/QtDeploySupport.cmake) include("${CMAKE_CURRENT_LIST_DIR}/MyProject-plugins.cmake" OPTIONAL) set(__QT_DEPLOY_ALL_MODULES_FOUND_VIA_FIND_PACKAGE "ZlibPrivate;EntryPointPrivate;Core;Gui;QmlIntegration;Network;Qml;QmlModels;OpenGL;Quick;Svg;Widgets") qt_deploy_qml_imports(TARGET MyProject PLUGINS_FOUND plugins_found NO_QT_IMPORTS #[Edited] To not be disturbed by Qt modules ) include(CMakePrintHelpers) cmake_print_variables(plugins_found ) qt_deploy_runtime_dependencies( EXECUTABLE <build-dir>/MyProject.exe ADDITIONAL_MODULES ${plugins_found} GENERATE_QT_CONF NO_TRANSLATIONS )
When installing this script, I found that "plugins_found" variable is empty. (and it's full of qt modules when NO_QT_IMPORTS isn't defined)
As I've seen from doc examples, and some projects from github, there isn't more thing to do to make it works.
That is my first experience in deploying a qml application, and I'm running out of ideas for this problem. Does anyone see something that I do wrong, or something missing ?
Note that I didn't publish the whole CMakeList.txt like including some external libs, that I think are not related to this issue. -
I've finally understand what was wrong in my project. I post the solution here hope this will help other people in futur !
I misunderstood how to work with a QML module that is integrated in the target.
As the qt-add-qml-module() documentation says about "Executable as a QML module",
"In this case, there will be no plugin library, since the QML module will always be loaded directly as part of the application."
This is why the _qt_internal_deploy_qml_imports_for_target() exclude the line of my internal module, and doesn't copy it at installation in the <install-dir>/qml/ directory.
Now I've understood why the qt_generate_deploy_qml_app_script doesn't install my module. But I was still anoyed about having to provide a qmlDir for this module in the install dir, while this module should already be a part of the binary ressources...
My mistake was that I called this API (from Qt6.5) to load the main QML file in my main.cpp, which search for DM_MainWindow.qml in QML Import Path
engine.loadFromModule("MyProject_Module", "DM_MainWindow.qml");
The qmlDir file, was usefull to the app because it make a link between the QML Resources's RelativePath+FileName and the FileName. Since, when passing QML Resources to qt_add_qml_module(), it's with the RelativePath+FileName format.
To fix my problem, I've found 2 solution :
- Solution 1 : Use old API to load QML Resource from Url :
engine.load(QUrl(QStringLiteral("qrc:/qt/qml/MyProject_Module/qml/DM_MainWindow.qml")));
where :
- "qrc:/qt/qml" is from RESOURCE_PREFIX "qt/qml/" declaration in qt_add_qml_module()
- "MyProject_Module" is from the URI ${PROJECT_NAME}_Module declaration in qt_add_qml_module()
- "qml/DM_MainWindow.qml" is from the QML Resource passed to qt_add_qml_module() in RelativePath+FileName format- Solution 2 : Use QT_RESOURCE_ALIAS property on QML Resource files to save the link made by the qmlDir file, directly in the binary. I've so modified my CMakeList.txt as follow :
set(QML_RESOURCES_PATH qml) set(QML_RESOURCES DM_MainWindow.qml DM_DeviceListSelector.qml ) foreach(qmlFile IN LISTS QML_RESOURCES) set_source_files_properties(${QML_RESOURCES_PATH}/${qmlFile} PROPERTIES QT_RESOURCE_ALIAS ${qmlFile}) endforeach() list(TRANSFORM QML_RESOURCES PREPEND "${QML_RESOURCES_PATH}/") qt_add_qml_module(${PROJECT_NAME} URI ${PROJECT_NAME}_Module VERSION 1.0 RESOURCE_PREFIX "qt/qml/" QML_FILES ${QML_RESOURCES} SOURCES ${SRC_PATH}/DM_Device.cpp #Some QML elements in C++ ${SRC_PATH}/DM_Device.h ) #No need to install module's directory then !
With any of these 2 solutions, I'm now able to install the app and launch it, without the MyProject_Module/qmlDir in the installation directory. I would prefer the Solution 2 because it use the most recent APIs, and seems more close to the documentation.
Thanks @Mesrine for your help !
-
I normally install that folder also using CMake install macro.
I do not know of any qt macro to copy that folder and i think the folder should be installed.
In your case, it should be likeinstall(DIRECTORY ${CMAKE_BINARY_DIR}/${PROJECT_NAME}_Module DESTINATION ${CMAKE_INSTALL_PREFIX}/qml )
-
Thank you for your answer ! Yes, this is what I think to do if I don't find other solutions.
The reason I created this topic, is because, according to the qt_generate_deploy_qml_app_script() documentation, I supposed that this command should have done it for me :
"Installing an executable target that is also a QML module requires deploying a number of things in addition to the target itself. Qt libraries and other libraries from the project, Qt plugins, and the runtime parts of all QML modules the application uses may all need to be installed too.
[...]
QML modules are installed to the appropriate location for the platform"Moreover, in the example provided, there isn't other lines after :
qt_generate_deploy_qml_app_script( TARGET MyApp OUTPUT_SCRIPT deploy_script #[...] ) install(deploy_script)
I've also noticed it in github projects such as addhoursandminutes, HLA-NoVR-Launcher or nheko projects.
Also, to be sure my module is well created, I've put this command just before the qt_generate_deploy_qml_app_script() :
qt_query_qml_module(${PROJECT_NAME} URI module_uri VERSION module_version PLUGIN_TARGET module_plugin_target TARGET_PATH module_target_path QMLDIR module_qmldir TYPEINFO module_typeinfo QML_FILES module_qml_files QML_FILES_DEPLOY_PATHS qml_files_deploy_paths RESOURCES module_resources RESOURCES_DEPLOY_PATHS resources_deploy_paths )
And this return me correct results and path :
[cmake] -- module_uri="MyProject_Module" [cmake] -- module_version="1.0" [cmake] -- module_plugin_target="" [cmake] -- module_target_path="MyProject_Module" [cmake] -- module_qmldir="<build-dir>/MyProject_Module/qmldir" [cmake] -- module_typeinfo="<build-dir>/MyProject_Module/MyProject.qmltypes" [cmake] -- module_qml_files="<project-dir>/qml/DM_MainWindow.qml;<project-dir>/qml/DM_DeviceListSelector.qml [cmake] -- qml_files_deploy_paths="qml/DM_MainWindow.qml;qml/DM_DeviceListSelector.qml" [cmake] -- module_resources="" [cmake] -- resources_deploy_paths=""
I've also inspected the commands and scripts generated under qt_generate_deploy_qml_app_script():
1- The qt_deploy_qml_imports() generate a cmake file under <build-dir>/.qt/deploy_qml_imports/MyProject.cmake, which contain this code :
# Auto-generated deploy QML imports script for target "MyProject". # Do not edit, all changes will be lost. # This file should only be included by qt_deploy_qml_imports(). set(__qt_opts ) if(arg_NO_QT_IMPORTS) list(APPEND __qt_opts NO_QT_IMPORTS) endif() _qt_internal_deploy_qml_imports_for_target( ${__qt_opts} IMPORTS_FILE "<build-dir>/.qt_plugins/Qt6_QmlPlugins_Imports_MyProject.cmake" PLUGINS_FOUND __qt_internal_plugins_found QML_DIR "${arg_QML_DIR}" PLUGINS_DIR "${arg_PLUGINS_DIR}" ) if(arg_PLUGINS_FOUND) set(${arg_PLUGINS_FOUND} "${__qt_internal_plugins_found}" PARENT_SCOPE) endif()
- The <build-dir>/.qt_plugins/Qt6_QmlPlugins_Imports_MyProject.cmake contains a list of Qt modules, including my MyProject_Module ! (See qml_import_scanner_import_29 ):
set(qml_import_scanner_imports_count 31) set(qml_import_scanner_import_0 "CLASSNAME;QtQuick2Plugin;LINKTARGET;Qt6::qtquick2plugin;NAME;QtQuick;PATH;C:/Qt/6.6.1/mingw_64/qml/QtQuick;PLUGIN;qtquick2plugin;PLUGINISOPTIONAL;;PREFER;:/qt-project.org/imports/QtQuick/;RELATIVEPATH;QtQuick;TYPE;module;") set(qml_import_scanner_import_1 "CLASSNAME;QtQmlMetaPlugin;[...]") [...] #Other Qt modules (qml_import_scanner_import_2...26) set(qml_import_scanner_import_27 "CLASSNAME;QtQuickLayoutsPlugin;[...]") set(qml_import_scanner_import_28 "NAME;QtQuick.Controls.Styles;TYPE;module;") set(qml_import_scanner_import_29 "COMPONENTS;<build-dir>/MyProject_Module/qml/DM_DeviceListSelector.qml;<build-dir>/MyProject_Module/qml/DM_MainWindow.qml;NAME;MyProject_Module;PATH;<build-dir>/MyProject_Module;PREFER;:/:/qt/qml/MyProject_Module/;RELATIVEPATH;MyProject_Module;TYPE;module;") set(qml_import_scanner_import_30 "CLASSNAME;QtLabsPlatformPlugin;[...]")
I don't have any warning (except a "Warning: using binaries from different directories" but not sure it's related), so I don't know why the _qt_internal_deploy_qml_imports_for_target() doesn't parse my MyProject_Module :(
-
I've finally understand what was wrong in my project. I post the solution here hope this will help other people in futur !
I misunderstood how to work with a QML module that is integrated in the target.
As the qt-add-qml-module() documentation says about "Executable as a QML module",
"In this case, there will be no plugin library, since the QML module will always be loaded directly as part of the application."
This is why the _qt_internal_deploy_qml_imports_for_target() exclude the line of my internal module, and doesn't copy it at installation in the <install-dir>/qml/ directory.
Now I've understood why the qt_generate_deploy_qml_app_script doesn't install my module. But I was still anoyed about having to provide a qmlDir for this module in the install dir, while this module should already be a part of the binary ressources...
My mistake was that I called this API (from Qt6.5) to load the main QML file in my main.cpp, which search for DM_MainWindow.qml in QML Import Path
engine.loadFromModule("MyProject_Module", "DM_MainWindow.qml");
The qmlDir file, was usefull to the app because it make a link between the QML Resources's RelativePath+FileName and the FileName. Since, when passing QML Resources to qt_add_qml_module(), it's with the RelativePath+FileName format.
To fix my problem, I've found 2 solution :
- Solution 1 : Use old API to load QML Resource from Url :
engine.load(QUrl(QStringLiteral("qrc:/qt/qml/MyProject_Module/qml/DM_MainWindow.qml")));
where :
- "qrc:/qt/qml" is from RESOURCE_PREFIX "qt/qml/" declaration in qt_add_qml_module()
- "MyProject_Module" is from the URI ${PROJECT_NAME}_Module declaration in qt_add_qml_module()
- "qml/DM_MainWindow.qml" is from the QML Resource passed to qt_add_qml_module() in RelativePath+FileName format- Solution 2 : Use QT_RESOURCE_ALIAS property on QML Resource files to save the link made by the qmlDir file, directly in the binary. I've so modified my CMakeList.txt as follow :
set(QML_RESOURCES_PATH qml) set(QML_RESOURCES DM_MainWindow.qml DM_DeviceListSelector.qml ) foreach(qmlFile IN LISTS QML_RESOURCES) set_source_files_properties(${QML_RESOURCES_PATH}/${qmlFile} PROPERTIES QT_RESOURCE_ALIAS ${qmlFile}) endforeach() list(TRANSFORM QML_RESOURCES PREPEND "${QML_RESOURCES_PATH}/") qt_add_qml_module(${PROJECT_NAME} URI ${PROJECT_NAME}_Module VERSION 1.0 RESOURCE_PREFIX "qt/qml/" QML_FILES ${QML_RESOURCES} SOURCES ${SRC_PATH}/DM_Device.cpp #Some QML elements in C++ ${SRC_PATH}/DM_Device.h ) #No need to install module's directory then !
With any of these 2 solutions, I'm now able to install the app and launch it, without the MyProject_Module/qmlDir in the installation directory. I would prefer the Solution 2 because it use the most recent APIs, and seems more close to the documentation.
Thanks @Mesrine for your help !
-
-
-
-
@lucast said in CMake installation of QML Module fail:
In this case, there will be no plugin library, since the QML module will always be loaded directly as part of the application."
True, you do not need to install the plugin target or the module dir in that case. Because it is compiled in the application. I was referring when installing a module library.
Solution 1 : Use old API to load QML Resource from Url :
I prefer solution 1, which is cleaner. Why do you say is the old API?
-
@Mesrine said in CMake installation of QML Module fail:
I prefer solution 1, which is cleaner. Why do you say is the old API?
I said that because the loadFromModule API is from Qt6.5, and load(Url()) is from Qt5. So loadFromModule appears to me to be the most recent way to load a qml resource from c++.
The name also match the CMake and QML strategy, which considers internal QML resources as a "module".This solution also avoid me to have the .qml files's relative path in the resource's url. I prefer to distinct them, and access files in resources directly by their names.
I may have used "oldest API" instead of "old API" !