diff --git a/docs/module/ECMAddQmlModule.rst b/docs/module/ECMAddQmlModule.rst new file mode 100644 --- /dev/null +++ b/docs/module/ECMAddQmlModule.rst @@ -0,0 +1 @@ +.. ecm-module:: ../../modules/ECMAddQmlModule.cmake diff --git a/modules/ECMAddQmlModule.cmake b/modules/ECMAddQmlModule.cmake new file mode 100644 --- /dev/null +++ b/modules/ECMAddQmlModule.cmake @@ -0,0 +1,588 @@ +#.rst: +# ECMAddQmlModule +# -------------------- +# +# Adds a QML module, while generating and installing also a matching +# "qmldir" file. +# +# :: +# +# ecm_add_qmlmodule( +# IDENTIFIER +# DESTINATION +# [PLUGIN ] +# [PATH ] +# [TYPEINFO ] +# [VERSION ] +# [DEPENDS [ [...]]] +# [DESIGNERSUPPORTED]) +# +# ``IDENTIFIER`` specifies the module identifier of the module. +# +# ``DESTINATION`` specifies the directory on disk to which the module +# will be installed, without the module-specific sub-path. +# If using the :kde-module:`KDEInstallDirs` module, one passes +# ``${KDE_INSTALL_QMLDIR}`` as value usually. +# +# ``PLUGIN`` specifies the target name of a QML extension plugin used +# for the module. +# +# ``PATH`` specifies the sub-path below the +# where the module will be installed. +# By default this is derived from . +# +# ``TYPEINFO`` specifies a type information file (``*.qmltypes``) that is +# part of the sources for the module. Such a file helps QML tools which cannot +# introspect the module (even more the binary plugin) themselves. +# The developers need to keep this file manually up-to-date. To assist that, +# when passing this argument a target "qmltypes--" +# is generated which can be used to update the type information file +# from the latest installed build of the module. This needs the tool +# "qmlplugindump" installed, otherwise no target will be created. +# +# ``VERSION`` specifies the module version of the module. +# Needed when using ``TYPEINFO``. +# +# ``DEPENDS`` specifies dependencies which should be noted in the "qmldir" +# file. Each ```` argument is a string with both the module identifier +# of the dependency and its version (e.g. ``"bar.example 1.0"``). +# +# ``DESIGNERSUPPORTED`` specifies that the "qmldir" file should get the +# ``designersupported`` flag added. +# +# Example usage: +# +# .. code-block:: cmake +# +# add_library(fooexampleplugin SHARED ${fooexampleplugin_SRCS}) +# +# target_link_libraries(fooexampleplugin +# Qt5::Qml +# ) +# +# ecm_add_qmlmodule(fooexample +# IDENTIFIER foo.example +# DESTINATION "${KDE_INSTALL_QMLDIR}" +# PLUGIN fooexampleplugin +# TYPEINFO fooexample.qmltypes +# VERSION 0.23 +# DEPENDS +# "bar.example 42.0" +# DESIGNERSUPPORTED +# ) +# +# :: +# +# ecm_qmlmodule_objecttypes( +# INITIAL_VERSION +# OBJECT_TYPES [singleton] [[singleton] [...]] +# [RELATIVE_PATH ]) +# +# ``INITIAL_VERSION`` specifies the module version for which the type is +# to be made available. +# +# ``OBJECT_TYPES`` specifies pairs of all the nodule's object types and their +# respective source QML files which define the type. +# +# ``RELATIVE_PATH`` specifies the relative path to which the QML files should +# be installed in the module. +# +# +# Example usage: +# +# .. code-block:: cmake +# +# ecm_add_qmlmodule(fooexample +# IDENTIFIER foo.example +# DESTINATION "${KDE_INSTALL_QMLDIR}" +# VERSION 0.23 +# ) +# +# ecm_qmlmodule_objecttypes(fooexample +# INITIAL_VERSION 0.23 +# RELATIVE_PATH foos +# OBJECT_TYPES +# Foo FooImpl.qml +# singleton FooMaster FooMasterImpl.qml +# ) +# +# :: +# +# ecm_qmlmodule_internalobjecttypes( +# OBJECT_TYPES [ [...]] +# [RELATIVE_PATH ]) +# +# ``OBJECT_TYPES`` specifies pairs of the nodule's internal object types and their +# respective source QML files which define the type. +# +# ``RELATIVE_PATH`` specifies the relative path to which the QML files should +# be installed in the module. +# +# +# Example usage: +# +# .. code-block:: cmake +# +# ecm_add_qmlmodule(fooexample +# IDENTIFIER foo.example +# DESTINATION "${KDE_INSTALL_QMLDIR}" +# VERSION 0.23 +# ) +# +# ecm_qmlmodule_internalobjecttypes(fooexample +# RELATIVE_PATH private +# OBJECT_TYPES +# PrivateFoo PrivateFooImpl.qml +# PrivateFooMaster PrivateFooMasterImpl.qml +# ) +# +# :: +# +# ecm_qmlmodule_jsresources( +# INITIAL_VERSION +# JS_RESOURCES [ [...]] +# [RELATIVE_PATH ]) +# +# ``INITIAL_VERSION`` specifies the module version for which the resource is +# to be made available. +# +# ``JS_RESOURCES`` specifies pairs of identifiers and the respective JavaScrupt +# resource source files. +# +# ``RELATIVE_PATH`` specifies the relative path to which the resource files should +# be installed in the module. +# +# +# Example usage: +# +# .. code-block:: cmake +# +# ecm_add_qmlmodule(fooexample +# IDENTIFIER foo.example +# DESTINATION "${KDE_INSTALL_QMLDIR}" +# VERSION 0.23 +# ) +# +# ecm_qmlmodule_jsresources(fooexample +# INITIAL_VERSION 0.23 +# RELATIVE_PATH code +# JS_RESOURCES +# FooScript js/FooScript.js +# FooMasterScript js/FooMasterScript.js +# ) +# +# :: +# +# ecm_qmlmodule_privatefiles( +# FILES [ [...]] +# DIRS [ [...]] +# [RELATIVE_PATH ]) +# +# ``FILES`` specifies source files which should be simply installed as part +# of the module. +# +# ``DIRS`` specifies complete source directories which should be simply +# installed as part of the module. +# +# ``RELATIVE_PATH`` specifies the relative path to which the files or dirs +# should be installed in the module. +# +# +# Example usage: +# +# .. code-block:: cmake +# +# ecm_add_qmlmodule(fooexample +# IDENTIFIER foo.example +# DESTINATION "${KDE_INSTALL_QMLDIR}" +# VERSION 0.23 +# ) +# +# ecm_qmlmodule_privatefiles(fooexample +# RELATIVE_PATH private +# FILES +# FooHelper.qml +# FooMasterHelper.qml +# ) +# +# Since 5.60.0. + + +#============================================================================= +# Copyright 2019 Friedrich W. H. Kossebau +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# helper function +function(_ecm_generate_typeinfo _identifier _version _typeinfo_file _plugin_base_dir) + set(_targetname "qmltypes-${_identifier}-${_version}") + + find_program(_qmlplugindump NAMES qmlplugindump-qt5 qmlplugindump) + + if (NOT _qmlplugindump) + message(STATUS "Could not find qmlplugindump. Will not create targets to update *.qmltypes files.") + else() + if (NOT IS_ABSOLUTE ${_plugin_base_dir}) + set(_plugin_base_dir "${CMAKE_INSTALL_PREFIX}/${_plugin_base_dir}") + endif() + + add_custom_target(${_targetname} + BYPRODUCTS ${_typeinfo_file} + COMMAND ${_qmlplugindump} -nonrelocatable ${_identifier} ${_version} ${_plugin_base_dir} > ${_typeinfo_file} + ) + + if (NOT TARGET qmltypes) + add_custom_target(qmltypes) + endif() + + add_dependencies(qmltypes ${_targetname}) + endif() +endfunction() + + +function(ecm_add_qmlmodule _target) + set(options + DESIGNERSUPPORTED + ) + set(oneValueArgs + DESTINATION + IDENTIFIER + TYPEINFO + VERSION + PLUGIN + PATH + ) + set(multiValueArgs + DEPENDS + ) + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # check args + if (TARGET ${_target}) + message(FATAL_ERROR "Target passed to ecm_add_qmlmodule already exists: ${_target}") + endif() + if (ARGS_PLUGIN AND NOT TARGET ${ARGS_PLUGIN}) + message(FATAL_ERROR "Unknown plugin target passed to ecm_add_qmlmodule: ${ARGS_PLUGIN}") + endif() + if(ARGS_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unexpected arguments to ecm_add_qmlmodule: ${ARGS_UNPARSED_ARGUMENTS}") + endif() + if(NOT ARGS_DESTINATION) + message(FATAL_ERROR "No DESTINATION argument given to ecm_add_qmlmodule") + endif() + if(NOT ARGS_IDENTIFIER) + message(FATAL_ERROR "No IDENTIFIER argument given to ecm_add_qmlmodule") + endif() + if(ARGS_TYPEINFO AND NOT ARGS_VERSION) + message(FATAL_ERROR "No VERSION argument given to ecm_add_qmlmodule (needed with TYPEINFO)") + endif() + + # target + add_custom_target(${_target} ALL) + + # prepare typeinfo file + if (ARGS_TYPEINFO) + if (NOT IS_ABSOLUTE ${ARGS_TYPEINFO}) + set(ARGS_TYPEINFO "${CMAKE_CURRENT_SOURCE_DIR}/${ARGS_TYPEINFO}") + endif() + _ecm_generate_typeinfo(${ARGS_IDENTIFIER} ${ARGS_VERSION} ${ARGS_TYPEINFO} ${ARGS_DESTINATION}) + endif() + + # generate qmldir file + set(_builddir "${CMAKE_CURRENT_BINARY_DIR}/${_target}_ECMAddQmlModule") + set(qmldir_FILE "${_builddir}/qmldir") # TODO: support multiple in same dir? + file(WRITE ${qmldir_FILE} "module ${ARGS_IDENTIFIER}\n") + set_target_properties(${_target} PROPERTIES ECM_QMLDIR_FILE "${qmldir_FILE}") + + if (ARGS_PLUGIN) + # TODO: is there a way to directly get the effective name? + get_target_property(_plugin_name ${ARGS_PLUGIN} LIBRARY_OUTPUT_NAME) + if(NOT _plugin_name) + get_target_property(_plugin_name ${ARGS_PLUGIN} OUTPUT_NAME) + endif() + if(NOT _plugin_name) + set(_plugin_name ${ARGS_PLUGIN}) + endif() + file(APPEND ${qmldir_FILE} "plugin ${_plugin_name}\n") + endif() + + if (EXISTS ${ARGS_TYPEINFO}) + get_filename_component(_typeinfo_filename "${ARGS_TYPEINFO}" NAME) + file(APPEND ${qmldir_FILE} "typeinfo ${_typeinfo_filename}\n") + endif() + + foreach(_depends ${ARGS_DEPENDS}) + file(APPEND ${qmldir_FILE} "depends ${_depends}\n") + endforeach() + + if(ARGS_DESIGNERSUPPORTED) + file(APPEND ${qmldir_FILE} "designersupported\n") + endif() + + # install + if(ARGS_PATH) + set(_module_path "${ARGS_PATH}") + else() + string(REPLACE "." "/" _module_path ${ARGS_IDENTIFIER}) + endif() + set(_install_dir "${ARGS_DESTINATION}/${_module_path}") + set_target_properties(${_target} PROPERTIES ECM_QML_INSTALLDIR "${_install_dir}") + + if (ARGS_PLUGIN) + install(TARGETS ${ARGS_PLUGIN} DESTINATION "${_install_dir}") + endif() + install(FILES ${qmldir_FILE} DESTINATION "${_install_dir}") + if (EXISTS ${ARGS_TYPEINFO}) + install(FILES ${ARGS_TYPEINFO} DESTINATION "${_install_dir}") + endif() +endfunction() + + +function(ecm_qmlmodule_objecttypes _target) + set(options + ) + set(oneValueArgs + INITIAL_VERSION + RELATIVE_PATH + ) + set(multiValueArgs + OBJECT_TYPES + ) + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # check args + if (NOT TARGET ${_target}) + message(FATAL_ERROR "Unknown target given to ecm_qmlmodule_objecttypes: ${_target}") + endif() + if(NOT ARGS_INITIAL_VERSION) + message(FATAL_ERROR "No INITIAL_VERSION argument given to ecm_qmlmodule_objecttypes") + endif() + if(NOT DEFINED ARGS_OBJECT_TYPES) + message(FATAL_ERROR "No ARGS_OBJECT_TYPES argument given to ecm_qmlmodule_objecttypes") + endif() + if(ARGS_RELATIVE_PATH AND NOT ARGS_RELATIVE_PATH MATCHES "/$") + string(APPEND ARGS_RELATIVE_PATH "/") + endif() + + get_target_property(_qmldir_file ${_target} ECM_QMLDIR_FILE) + get_target_property(_install_dir ${_target} ECM_QML_INSTALLDIR) + + # parse object types + if (ARGS_OBJECT_TYPES) + file(APPEND ${_qmldir_file} "\n") + endif() + set(_qml_files) + set(_parse_state "SINGLETONORTYPENAME") + set(_typename) + set(_singleton) + foreach(_value IN LISTS ARGS_OBJECT_TYPES) + if(_parse_state STREQUAL "FILENAME") + set(_qml_file "${_value}") + if (IS_ABSOLUTE ${_qml_file}) + set(_qml_filepath "${_qml_file}") + else() + set(_qml_filepath "${CMAKE_CURRENT_SOURCE_DIR}/${_qml_file}") + endif() + if (NOT EXISTS ${_qml_filepath}) + message(FATAL_ERROR "Not existing file passed to OBJECT_TYPES of ecm_qmlmodule_objecttypes: ${_qml_file}") + endif() + get_filename_component(_qml_filename "${_qml_filepath}" NAME) + file(APPEND ${_qmldir_file} "${_singleton}${_typename} ${ARGS_INITIAL_VERSION} ${ARGS_RELATIVE_PATH}${_qml_filename}\n") + list(APPEND _qml_files "${_qml_file}") + # reset + set(_singleton) + set(_parse_state "SINGLETONORTYPENAME") + else() + if(_parse_state STREQUAL "SINGLETONORTYPENAME") + if(_value STREQUAL "singleton") + set(_singleton "singleton ") + set(_parse_state "TYPENAME") + continue() + endif() + endif() + set(_typename ${_value}) + set(_parse_state "FILENAME") + endif() + endforeach() + if(NOT _parse_state STREQUAL "SINGLETONORTYPENAME") + message(FATAL_ERROR "Wrong number of arguments passed to OBJECT_TYPES of ecm_qmlmodule_objecttypes") + endif() + + # install + install(FILES ${_qml_files} DESTINATION "${_install_dir}/${ARGS_RELATIVE_PATH}") +endfunction() + + +function(ecm_qmlmodule_internalobjecttypes _target) + set(options + ) + set(oneValueArgs + RELATIVE_PATH + ) + set(multiValueArgs + OBJECT_TYPES + ) + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # check args + if (NOT TARGET ${_target}) + message(FATAL_ERROR "Unknown target given to ecm_qmlmodule_internalobjecttypes: ${_target}") + endif() + if(NOT DEFINED ARGS_OBJECT_TYPES) + message(FATAL_ERROR "No ARGS_OBJECT_TYPES argument given to ecm_qmlmodule_internalobjecttypes") + endif() + if(ARGS_RELATIVE_PATH AND NOT ARGS_RELATIVE_PATH MATCHES "/$") + string(APPEND ARGS_RELATIVE_PATH "/") + endif() + + get_target_property(_qmldir_file ${_target} ECM_QMLDIR_FILE) + get_target_property(_install_dir ${_target} ECM_QML_INSTALLDIR) + + # parse object types + if (ARGS_OBJECT_TYPES) + file(APPEND ${_qmldir_file} "\n") + endif() + set(_qml_files) + set(_parse_state "TYPENAME") + set(_typename) + foreach(_value IN LISTS ARGS_OBJECT_TYPES) + if(_parse_state STREQUAL "FILENAME") + set(_qml_file "${_value}") + if (IS_ABSOLUTE ${_qml_file}) + set(_qml_filepath "${_qml_file}") + else() + set(_qml_filepath "${CMAKE_CURRENT_SOURCE_DIR}/${_qml_file}") + endif() + if (NOT EXISTS ${_qml_filepath}) + message(FATAL_ERROR "Not existing file passed to OBJECT_TYPES of ecm_qmlmodule_internalobjecttypes: ${_qml_file}") + endif() + get_filename_component(_qml_filename "${_qml_filepath}" NAME) + file(APPEND ${_qmldir_file} "internal ${_typename} ${ARGS_RELATIVE_PATH}${_qml_filename}\n") + list(APPEND _qml_files "${_qml_file}") + # reset + set(_parse_state "TYPENAME") + else() + set(_typename ${_value}) + set(_parse_state "FILENAME") + endif() + endforeach() + if(NOT _parse_state STREQUAL "TYPENAME") + message(FATAL_ERROR "Wrong number of arguments passed to OBJECT_TYPES of ecm_qmlmodule_internalobjecttypes") + endif() + + # install + install(FILES ${_qml_files} DESTINATION "${_install_dir}/${ARGS_RELATIVE_PATH}") +endfunction() + + +function(ecm_qmlmodule_jsresources _target) + set(options + ) + set(oneValueArgs + INITIAL_VERSION + RELATIVE_PATH + ) + set(multiValueArgs + JS_RESOURCES + ) + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # check args + if (NOT TARGET ${_target}) + message(FATAL_ERROR "Unknown target given to ecm_qmlmodule_jsresources: ${_target}") + endif() + if(NOT ARGS_INITIAL_VERSION) + message(FATAL_ERROR "No INITIAL_VERSION argument given to ecm_qmlmodule_jsresources") + endif() + if(NOT DEFINED ARGS_JS_RESOURCES) + message(FATAL_ERROR "No ARGS_JS_RESOURCES argument given to ecm_qmlmodule_jsresources") + endif() + if(ARGS_RELATIVE_PATH AND NOT ARGS_RELATIVE_PATH MATCHES "/$") + string(APPEND ARGS_RELATIVE_PATH "/") + endif() + + get_target_property(_qmldir_file ${_target} ECM_QMLDIR_FILE) + get_target_property(_install_dir ${_target} ECM_QML_INSTALLDIR) + + # parse + set(_qml_files) + set(_parse_state "RESOURCEID") + set(_resourceid) + foreach(_value IN LISTS ARGS_JS_RESOURCES) + if(_parse_state STREQUAL "RESOURCEID") + set(_resourceid ${_value}) + set(_parse_state "FILENAME") + else() + set(_resource_file "${_value}") + if (NOT IS_ABSOLUTE ${_resource_file}) + set(_resource_file "${CMAKE_CURRENT_SOURCE_DIR}/${_resource_file}") + endif() + if (NOT EXISTS ${_resource_file}) + message(FATAL_ERROR "Not existing file passed to JS_RESOURCES of ecm_qmlmodule_jsresources: ${_value}") + endif() + get_filename_component(_resource_filename "${_resource_file}" NAME) + file(APPEND ${_qmldir_file} "${_resourceid} ${ARGS_INITIAL_VERSION} ${ARGS_RELATIVE_PATH}${_resource_filename}\n") + set(_parse_state "RESOURCEID") + endif() + endforeach() + if(NOT _parse_state STREQUAL "RESOURCEID") + message(FATAL_ERROR "Wrong number of arguments passed to JS_RESOURCES of ecm_qmlmodule_jsresources") + endif() + + # install + install(FILES ${_qml_files} DESTINATION "${_install_dir}/${ARGS_RELATIVE_PATH}") +endfunction() + +function(ecm_qmlmodule_privatefiles _target) + set(options + ) + set(oneValueArgs + RELATIVE_PATH + ) + set(multiValueArgs + FILES + DIRS + ) + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # check args + if (NOT TARGET ${_target}) + message(FATAL_ERROR "Unknown target given to ecm_qmlmodule_privatefiles: ${_target}") + endif() + if(NOT DEFINED ARGS_FILES AND NOT DEFINED ARGS_DIRS) + message(FATAL_ERROR "No ARGS_FILES OR ARGS_DIRS argument given to ecm_qmlmodule_privatefiles") + endif() + if(ARGS_RELATIVE_PATH AND NOT ARGS_RELATIVE_PATH MATCHES "/$") + string(APPEND ARGS_RELATIVE_PATH "/") + endif() + + get_target_property(_install_dir ${_target} ECM_QML_INSTALLDIR) + + # install + install(FILES ${ARGS_FILES} DESTINATION "${_install_dir}/${ARGS_RELATIVE_PATH}") + foreach(_dir IN LISTS ARGS_DIRS) + install(DIRECTORY ${_dir} DESTINATION "${_install_dir}/${ARGS_RELATIVE_PATH}") + endforeach() +endfunction()