diff options
author | Craig Scott <craig.scott@qt.io> | 2021-11-30 14:34:29 +1100 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2021-12-13 19:41:38 +0000 |
commit | 1fe5ee6395abc94dde41642a48a31aab33ddbe2f (patch) | |
tree | 27bc3d4764051165609e7b2f0a3f462b9438b1bb | |
parent | 2c33b5d0b449b90993312e86e0904931e516dc2d (diff) |
Add CMake deploy support for QML module apps
Task-number: QTBUG-98545
Change-Id: I2d04ccbae0288c88ada399552e8f9c20e221b21d
Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
(cherry picked from commit e954a7f69491b72d1ad9144c20f66713b5017940)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r-- | src/qml/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/qml/Qt6QmlConfigExtras.cmake.in | 29 | ||||
-rw-r--r-- | src/qml/Qt6QmlDeploySupport.cmake | 223 | ||||
-rw-r--r-- | src/qml/Qt6QmlMacros.cmake | 428 | ||||
-rw-r--r-- | tools/qmlimportscanner/main.cpp | 22 |
5 files changed, 630 insertions, 73 deletions
diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index b3e8899ffa..65667b865d 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -394,6 +394,7 @@ qt_internal_add_qml_module(Qml "${CMAKE_CURRENT_LIST_DIR}/${INSTALL_CMAKE_NAMESPACE}qmldirTemplate.cmake.in" "${CMAKE_CURRENT_LIST_DIR}/${INSTALL_CMAKE_NAMESPACE}QmlPluginTemplate.cpp.in" "${CMAKE_CURRENT_LIST_DIR}/${INSTALL_CMAKE_NAMESPACE}QmlFindQmlscInternal.cmake" + "${CMAKE_CURRENT_LIST_DIR}/${INSTALL_CMAKE_NAMESPACE}QmlDeploySupport.cmake" ${extra_cmake_files} EXTRA_CMAKE_INCLUDES "${INSTALL_CMAKE_NAMESPACE}QmlFindQmlscInternal.cmake" diff --git a/src/qml/Qt6QmlConfigExtras.cmake.in b/src/qml/Qt6QmlConfigExtras.cmake.in index 194dc9eef2..1275624cfd 100644 --- a/src/qml/Qt6QmlConfigExtras.cmake.in +++ b/src/qml/Qt6QmlConfigExtras.cmake.in @@ -1,15 +1,22 @@ -if(NOT QT_NO_CREATE_TARGETS AND - NOT "@BUILD_SHARED_LIBS@" AND # Only needed if Qt was built statically - CMAKE_VERSION VERSION_GREATER_EQUAL 3.18) # Finalizers require cmake_language(CALL) - set(target @QT_CMAKE_EXPORT_NAMESPACE@::Qml) - get_property(aliased_target TARGET ${target} PROPERTY ALIASED_TARGET) - if(aliased_target) - set(target "${aliased_target}") +if(NOT QT_NO_CREATE_TARGETS) + set(__qt_qml_target @QT_CMAKE_EXPORT_NAMESPACE@::Qml) + get_property(__qt_qml_aliased_target TARGET ${__qt_qml_target} PROPERTY ALIASED_TARGET) + if(__qt_qml_aliased_target) + set(__qt_qml_target "${__qt_qml_aliased_target}") endif() - set_property(TARGET ${target} PROPERTY - INTERFACE_QT_EXECUTABLE_FINALIZERS - qt@PROJECT_VERSION_MAJOR@_import_qml_plugins - ) + if("@BUILD_SHARED_LIBS@") + set_property(TARGET ${__qt_qml_target} PROPERTY + INTERFACE_QT_EXECUTABLE_FINALIZERS + _qt_internal_generate_deploy_qml_imports_script + ) + else() + set_property(TARGET ${__qt_qml_target} PROPERTY + INTERFACE_QT_EXECUTABLE_FINALIZERS + qt@PROJECT_VERSION_MAJOR@_import_qml_plugins + ) + endif() + unset(__qt_qml_target) + unset(__qt_qml_aliased_target) endif() if(ANDROID) diff --git a/src/qml/Qt6QmlDeploySupport.cmake b/src/qml/Qt6QmlDeploySupport.cmake new file mode 100644 index 0000000000..58ca93b5fa --- /dev/null +++ b/src/qml/Qt6QmlDeploySupport.cmake @@ -0,0 +1,223 @@ +# NOTE: This code should only ever be executed in script mode. It expects to be +# used either as part of an install(CODE) call or called by a script +# invoked via cmake -P as a POST_BUILD step. It would not normally be +# included directly, it should be pulled in automatically by the deploy +# support set up by qtbase. + +cmake_minimum_required(VERSION 3.16...3.21) + +# This function is currently in Technical Preview. +# Its signature and behavior might change. +function(qt_deploy_qml_imports) + set(no_value_options + NO_QT_IMPORTS + ) + set(single_value_options + TARGET + PLUGINS_DIR # Internal option, only used for macOS app bundle targets + QML_DIR + PLUGINS_FOUND # Name of an output variable + ) + set(multi_value_options "") + cmake_parse_arguments(PARSE_ARGV 0 arg + "${no_value_options}" "${single_value_options}" "${multi_value_options}" + ) + + if(arg_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unparsed arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + + if(NOT arg_TARGET) + message(FATAL_ERROR "TARGET must be specified") + endif() + + if(NOT arg_QML_DIR) + set(arg_QML_DIR "${QT_DEPLOY_QML_DIR}") + endif() + + if(NOT arg_PLUGINS_DIR) + set(arg_PLUGINS_DIR "${QT_DEPLOY_PLUGINS_DIR}") + endif() + + # The target's finalizer should have written out this file + string(MAKE_C_IDENTIFIER "${arg_TARGET}" target_id) + set(filename "${__QT_DEPLOY_IMPL_DIR}/deploy_qml_imports/${target_id}") + if(__QT_DEPLOY_GENERATOR_IS_MULTI_CONFIG) + string(APPEND filename "-${__QT_DEPLOY_ACTIVE_CONFIG}") + endif() + string(APPEND filename ".cmake") + if(NOT EXISTS "${filename}") + message(FATAL_ERROR + "No QML imports information recorded for target ${arg_TARGET}. " + "The target must be an executable and qt_add_qml_module() must " + "have been called with it. Missing file:\n ${filename}" + ) + endif() + include(${filename}) + +endfunction() + +function(_qt_internal_deploy_qml_imports_for_target) + set(no_value_options + BUNDLE + NO_QT_IMPORTS + ) + set(single_value_options + IMPORTS_FILE + PLUGINS_FOUND + QML_DIR + PLUGINS_DIR + ) + set(multi_value_options "") + + cmake_parse_arguments(PARSE_ARGV 0 arg + "${no_value_options}" "${single_value_options}" "${multi_value_options}" + ) + + if(arg_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unexpected arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + foreach(opt IN LISTS single_value_options) + if(NOT arg_${opt}) + message(FATAL_ERROR "Required argument not provided: ${opt}") + endif() + endforeach() + + include("${arg_IMPORTS_FILE}") + + macro(_qt_internal_parse_qml_imports_entry prefix index) + cmake_parse_arguments("${prefix}" + "" + "CLASSNAME;NAME;PATH;PLUGIN;RELATIVEPATH;TYPE;VERSION;LINKTARGET" + "" + ${qml_import_scanner_import_${index}} + ) + endmacro() + + get_filename_component(install_prefix_abs "${QT_DEPLOY_PREFIX}" ABSOLUTE) + set(plugins_found "") + + if(__QT_DEPLOY_POST_BUILD) + message(STATUS "Running macOS bundle QML support POST_BUILD routine.") + endif() + + # Parse the generated cmake file. It is possible for the scanner to find no + # usage of QML, in which case the import count is 0. + if(qml_import_scanner_imports_count GREATER 0) + set(processed_names "") + math(EXPR last_index "${qml_import_scanner_imports_count} - 1") + foreach(index RANGE 0 ${last_index}) + _qt_internal_parse_qml_imports_entry(entry ${index}) + + if("${entry_NAME}" STREQUAL "") + message(WARNING "No NAME at scan index ${index}: ${imports_file}") + continue() + endif() + + # A plugin might have multiple entries (e.g. for different versions). + # Only copy it once. + if("${entry_NAME}" IN_LIST processed_names) + continue() + endif() + # Even if we skip this one, we don't need to process it again + list(APPEND processed_names ${entry_NAME}) + + if("${entry_PATH}" STREQUAL "" OR + "${entry_PLUGIN}" STREQUAL "" OR + "${entry_RELATIVEPATH}" STREQUAL "") + # These might be a valid QML module, but not have a plugin library. + # We only care about modules that have a plugin we need to copy. + continue() + endif() + + if(arg_NO_QT_IMPORTS AND + "${entry_LINKTARGET}" MATCHES "${__QT_CMAKE_EXPORT_NAMESPACE}::") + continue() + endif() + + # For installation, we want the qmldir file and its plugin. If the + # CMake project generating the plugin sets version details on its + # CMake target, we might have symlinks and version numbers in the + # file names, so account for those. There should never be plugin + # libraries for more than one QML module in the directory, so we + # shouldn't need to worry about matching plugins we don't want. + set(dest_qmldir "${QT_DEPLOY_PREFIX}/${arg_QML_DIR}/${entry_RELATIVEPATH}") + if(arg_BUNDLE) + set(dest_plugin "${QT_DEPLOY_PREFIX}/${arg_PLUGINS_DIR}") + else() + set(dest_plugin "${dest_qmldir}") + endif() + + file(INSTALL "${entry_PATH}/qmldir" DESTINATION "${dest_qmldir}") + + if(__QT_DEPLOY_POST_BUILD) + # We are being invoked as a post-build step. The plugin might + # not exist yet, so we can't even glob for it, let alone copy + # it. We know what its name should be though, so we can create + # a symlink to where it will eventually be, which will be enough + # to allow it to run from the build tree. It won't matter if + # the plugin gets updated later in the build, the symlink will + # still be pointing at the right location. + # In theory, this could be possible for any platform that + # supports symlinks (which all do in some form now, even + # Windows if the right permissions are set), but we only really + # expect to need this for macOS app bundles. + set(final_destination "${dest_qmldir}/lib${entry_PLUGIN}.dylib") + message(STATUS "Symlinking: ${final_destination}") + file(CREATE_LINK + "${entry_PATH}/lib${entry_PLUGIN}.dylib" + "${final_destination}" + SYMBOLIC + ) + + # We don't add this plugin to plugins_found because we don't + # actually make a copy of the plugin. We don't want the caller + # thinking they should further process what would still be the + # original plugin in the build tree. + continue() + endif() + + file(GLOB files LIST_DIRECTORIES false "${entry_PATH}/*${entry_PLUGIN}*") + list(FILTER files + INCLUDE REGEX "^(.*/)?(lib)?${entry_PLUGIN}.*\\.(so|dylib|dll)(\\.[0-9]+)*$") + file(INSTALL ${files} DESTINATION "${dest_plugin}" USE_SOURCE_PERMISSIONS) + + get_filename_component(dest_plugin_abs "${dest_plugin}" ABSOLUTE) + file(RELATIVE_PATH rel_path "${install_prefix_abs}" "${dest_plugin_abs}") + foreach(file IN LISTS files) + get_filename_component(filename "${file}" NAME) + list(APPEND plugins_found "${rel_path}/${filename}") + endforeach() + + if(arg_BUNDLE) + # Actual plugin binaries will be in PlugIns, but qmldir files + # expect them to be in the same directory as themselves + # (i.e. under Resources/qml/...). Add a symlink at the place + # the qmldir expects the binary to be. This arrangement keeps + # binaries under PlugIns and non-binaries under Resources, + # which is required for code signing to work properly. + get_filename_component(dest_qmldir_abs "${dest_qmldir}" ABSOLUTE) + file(RELATIVE_PATH rel_path "${dest_qmldir_abs}" "${dest_plugin_abs}") + foreach(plugin_file IN LISTS files) + get_filename_component(filename "${plugin_file}" NAME) + + set(final_destination "${dest_qmldir}/${filename}") + message(STATUS "Symlinking: ${final_destination}") + file(CREATE_LINK "${rel_path}/${filename}" "${final_destination}" SYMBOLIC) + endforeach() + endif() + endforeach() + endif() + + set(${arg_PLUGINS_FOUND} ${plugins_found} PARENT_SCOPE) + +endfunction() + +function(_qt_internal_show_skip_qml_runtime_deploy_message) + # Don't show the message in static Qt builds, it can be misleading, because we still + # run qmlimportscanner / link the static qml plguins into the binary despite not having + # a qml deployment step. + if(__QT_DEPLOY_IS_SHARED_LIBS_BUILD) + message(STATUS "Skipping QML module deployment steps.") + endif() +endfunction() diff --git a/src/qml/Qt6QmlMacros.cmake b/src/qml/Qt6QmlMacros.cmake index b80a2db126..a91fc831c5 100644 --- a/src/qml/Qt6QmlMacros.cmake +++ b/src/qml/Qt6QmlMacros.cmake @@ -4,6 +4,12 @@ set(__qt_qml_macros_module_base_dir "${CMAKE_CURRENT_LIST_DIR}" CACHE INTERNAL "") +# Install support uses the CMAKE_INSTALL_xxxDIR variables. Include this here +# so that it is more likely to get pulled in earlier at a higher level, and also +# to avoid re-including it many times later +include(GNUInstallDirs) +_qt_internal_add_deploy_support("${CMAKE_CURRENT_LIST_DIR}/Qt6QmlDeploySupport.cmake") + function(qt6_add_qml_module target) set(args_option STATIC @@ -19,6 +25,7 @@ function(qt6_add_qml_module target) NO_LINT NO_CACHEGEN NO_RESOURCE_TARGET_PATH + NO_IMPORT_SCAN # TODO: Remove once all usages have also been removed SKIP_TYPE_REGISTRATION @@ -430,6 +437,7 @@ function(qt6_add_qml_module target) QT_QML_MODULE_NO_GENERATE_QMLDIR "${arg_NO_GENERATE_QMLDIR}" QT_QML_MODULE_NO_PLUGIN "${arg_NO_PLUGIN}" QT_QML_MODULE_NO_PLUGIN_OPTIONAL "${arg_NO_PLUGIN_OPTIONAL}" + QT_QML_MODULE_NO_IMPORT_SCAN "${arg_NO_IMPORT_SCAN}" QT_QML_MODULE_FOLLOW_FOREIGN_VERSIONING "${arg_FOLLOW_FOREIGN_VERSIONING}" QT_QML_MODULE_URI "${arg_URI}" QT_QML_MODULE_TARGET_PATH "${arg_TARGET_PATH}" @@ -1244,6 +1252,14 @@ function(qt6_add_qml_plugin target) endif() endif() + # TODO: Probably should remove TARGET_PATH as a supported keyword now + if(NOT arg_TARGET_PATH AND TARGET "${arg_BACKING_TARGET}") + get_target_property(arg_TARGET_PATH ${arg_BACKING_TARGET} QT_QML_MODULE_TARGET_PATH) + endif() + if(NOT arg_TARGET_PATH) + string(REPLACE "." "/" arg_TARGET_PATH "${arg_URI}") + endif() + _qt_internal_get_escaped_uri("${arg_URI}" escaped_uri) if(NOT arg_CLASS_NAME) @@ -1339,6 +1355,40 @@ function(qt6_add_qml_plugin target) ) endif() + # Ignore any CMAKE_INSTALL_RPATH and set a better default RPATH on platforms + # that support it, if allowed. Projects will often set CMAKE_INSTALL_RPATH + # for executables or backing libraries, but forget about plugins. Because + # the path for QML plugins depends on their URI, it is unlikely that + # CMAKE_INSTALL_RPATH would ever be intended for use with QML plugins. + if(NOT WIN32 AND NOT QT_NO_QML_PLUGIN_RPATH) + # Construct a relative path from a default install location (assumed to + # be qml/target-path) to ${CMAKE_INSTALL_LIBDIR}. This would be + # applicable for Apple too (although unusual) if this is a bare install + # (i.e. not part of an app bundle). + string(REPLACE "/" ";" path "qml/${arg_TARGET_PATH}") + list(LENGTH path path_count) + string(REPEAT "../" ${path_count} rel_path) + string(APPEND rel_path "${CMAKE_INSTALL_LIBDIR}") + if(APPLE) + set(install_rpath + # If embedded in an app bundle, search in a bundle-local path + # first. This path should always be the same for every app + # bundle because plugin binaries should live in the PlugIns + # directory, not a subdirectory of it or anywhere else. + # Similarly, frameworks and bare shared libraries should always + # be in the bundle's Frameworks directory. + "@loader_path/../Frameworks" + + # This will be needed if the plugin is not installed as part of + # an app bundle, such as when used by a command-line tool. + "@loader_path/${rel_path}" + ) + else() + set(install_rpath "$ORIGIN/${rel_path}") + endif() + set_target_properties(${target} PROPERTIES INSTALL_RPATH "${install_rpath}") + endif() + get_target_property(moc_opts ${target} AUTOMOC_MOC_OPTIONS) set(already_set FALSE) if(moc_opts) @@ -2227,31 +2277,17 @@ if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) endif() -# This function is called as a finalizer in qt6_finalize_executable() for any -# target that links against the Qml library for a statically built Qt. -function(qt6_import_qml_plugins target) - if(QT6_IS_SHARED_LIBS_BUILD) - return() +function(_qt_internal_scan_qml_imports target imports_file_var when_to_scan) + if(NOT "${ARGN}" STREQUAL "") + message(FATAL_ERROR "Unknown/unexpected arguments: ${ARGN}") endif() - # Protect against being called multiple times in case we are being called - # explicitly before the finalizer is invoked. - get_target_property(alreadyImported ${target} _QT_QML_PLUGINS_IMPORTED) - if(alreadyImported) - return() - endif() - set_target_properties(${target} PROPERTIES _QT_QML_PLUGINS_IMPORTED TRUE) - - set(options) - set(oneValueArgs "PATH_TO_SCAN") # Internal option, may be removed - set(multiValueArgs) - - cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - if(NOT arg_PATH_TO_SCAN) - set(arg_PATH_TO_SCAN "${CMAKE_CURRENT_SOURCE_DIR}") - endif() - if(arg_UNPARSED_ARGUMENTS) - message(FATAL_ERROR "Unknown/unexpected arguments: ${arg_UNPARSED_ARGUMENTS}") + if(when_to_scan STREQUAL "BUILD_PHASE") + set(scan_at_build_time TRUE) + elseif(when_to_scan STREQUAL "IMMEDIATELY") + set(scan_at_build_time FALSE) + else() + message(FATAL_ERROR "Unexpected value for when_to_scan: ${when_to_scan}") endif() # Find location of qmlimportscanner. @@ -2259,7 +2295,8 @@ function(qt6_import_qml_plugins target) if(NOT tool_path) set(configs "RELWITHDEBINFO;RELEASE;MINSIZEREL;DEBUG") foreach(config ${configs}) - get_target_property(tool_path Qt6::qmlimportscanner IMPORTED_LOCATION_${config}) + get_target_property(tool_path + ${QT_CMAKE_EXPORT_NAMESPACE}::qmlimportscanner IMPORTED_LOCATION_${config}) if(tool_path) break() endif() @@ -2281,28 +2318,18 @@ but this file does not exist. Possible reasons include: # pass to qmlimportscanner. set(qml_path "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_QML}") - # Small macro to avoid duplicating code in two different loops. - macro(_qt6_QmlImportScanner_parse_entry) - # TODO: Should CLASSNAME be changed to CLASS_NAME? It is generated by - # the qmlimportscanner, not CMake code. - set(entry_name "qml_import_scanner_import_${idx}") - cmake_parse_arguments("entry" - "" - "CLASSNAME;NAME;PATH;PLUGIN;RELATIVEPATH;TYPE;VERSION;LINKTARGET" - "" - ${${entry_name}} - ) - endmacro() - - # Run qmlimportscanner and include the generated cmake file. - set(qml_imports_file_path - "${CMAKE_CURRENT_BINARY_DIR}/.qt_plugins/Qt6_QmlPlugins_Imports_${target}.cmake" - ) - file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/.qt_plugins) + # Run qmlimportscanner to generate the cmake file that records the import entries + get_target_property(target_source_dir ${target} SOURCE_DIR) + get_target_property(target_binary_dir ${target} BINARY_DIR) + set(out_dir "${target_binary_dir}/.qt_plugins") + set(imports_file "${out_dir}/Qt6_QmlPlugins_Imports_${target}.cmake") + set(${imports_file_var} "${imports_file}" PARENT_SCOPE) + file(MAKE_DIRECTORY ${out_dir}) set(cmd_args - -rootPath "${arg_PATH_TO_SCAN}" + -rootPath "${target_source_dir}" -cmake-output + -output-file "${imports_file}" -importPath "${qml_path}" ) get_target_property(qml_import_path ${target} QT_QML_IMPORT_PATH) @@ -2339,40 +2366,85 @@ but this file does not exist. Possible reasons include: # of arguments on the command line string(LENGTH "${cmd_args}" length) if(length GREATER 240) - set(rsp_file ${CMAKE_CURRENT_BINARY_DIR}/.qt_plugins/Qt6_QmlPlugins_Imports_${target}.rsp) + set(rsp_file "${out_dir}/Qt6_QmlPlugins_Imports_${target}.rsp") list(JOIN cmd_args "\n" rsp_file_content) file(WRITE ${rsp_file} "${rsp_file_content}") set(cmd_args "@${rsp_file}") endif() - get_target_property(target_source_dir ${target} SOURCE_DIR) - - message(VERBOSE "Running qmlimportscanner to find QML plugins needed by ${target}.") set(import_scanner_args ${QT_TOOL_COMMAND_WRAPPER_PATH} ${tool_path} ${cmd_args}) - list(JOIN import_scanner_args " " import_scanner_args_string) - message(DEBUG "qmlimportscanner command: ${import_scanner_args_string}") - execute_process(COMMAND ${import_scanner_args} - OUTPUT_FILE "${qml_imports_file_path}" - WORKING_DIRECTORY ${target_source_dir} - ) - include("${qml_imports_file_path}" OPTIONAL RESULT_VARIABLE qml_imports_file_path_found) - if(NOT qml_imports_file_path_found) - message(FATAL_ERROR - "Could not find ${qml_imports_file_path} which was supposed to be " - "generated by qmlimportscanner after processing target ${target}." + if(scan_at_build_time) + add_custom_command( + OUTPUT "${imports_file}" + COMMENT "Running qmlimportscanner for ${target}" + COMMAND ${import_scanner_args} + WORKING_DIRECTORY ${target_source_dir} + DEPENDS + ${tool_path} + ${qrc_files} + $<TARGET_PROPERTY:${target},QT_QML_MODULE_QML_FILES> + ) + add_custom_target(${target}_qmlimportscan DEPENDS "${imports_file}") + add_dependencies(${target} ${target}_qmlimportscan) + else() + message(VERBOSE "Running qmlimportscanner for ${target}.") + list(JOIN import_scanner_args " " import_scanner_args_string) + message(DEBUG "qmlimportscanner command: ${import_scanner_args_string}") + execute_process( + COMMAND ${import_scanner_args} + WORKING_DIRECTORY ${target_source_dir} + RESULT_VARIABLE result ) + if(result) + message(FATAL_ERROR + "Failed to scan target ${target} for QML imports: ${result}" + ) + endif() + endif() +endfunction() + +# Parse the entry at the specified index, assuming the caller already included +# the file generated by a call to _qt_internal_scan_qml_imports() +macro(_qt_internal_parse_qml_imports_entry prefix index) + cmake_parse_arguments("${prefix}" + "" + "CLASSNAME;NAME;PATH;PLUGIN;RELATIVEPATH;TYPE;VERSION;LINKTARGET" + "" + ${qml_import_scanner_import_${index}} + ) +endmacro() + + +# This function is called as a finalizer in qt6_finalize_executable() for any +# target that links against the Qml library for a statically built Qt. +function(qt6_import_qml_plugins target) + if(QT6_IS_SHARED_LIBS_BUILD) + return() + endif() + + # Protect against being called multiple times in case we are being called + # explicitly before the finalizer is invoked. + get_target_property(already_imported ${target} _QT_QML_PLUGINS_IMPORTED) + get_target_property(no_import_scan ${target} QT_QML_MODULE_NO_IMPORT_SCAN) + if(already_imported OR no_import_scan) + return() endif() + set_target_properties(${target} PROPERTIES _QT_QML_PLUGINS_IMPORTED TRUE) + + _qt_internal_scan_qml_imports(${target} imports_file IMMEDIATELY) + include("${imports_file}") # Parse the generated cmake file. # It is possible for the scanner to find no usage of QML, in which case the import count is 0. - if(qml_import_scanner_imports_count) + if(qml_import_scanner_imports_count GREATER 0) set(added_plugins "") set(plugins_to_link "") set(plugin_inits_to_link "") - foreach(idx RANGE "${qml_import_scanner_imports_count}") - _qt6_QmlImportScanner_parse_entry() + math(EXPR last_index "${qml_import_scanner_imports_count} - 1") + foreach(index RANGE 0 ${last_index}) + _qt_internal_parse_qml_imports_entry(entry ${index}) if(entry_PATH AND entry_PLUGIN) # Sometimes a plugin appears multiple times with different versions. # Make sure to process it only once. @@ -2437,6 +2509,240 @@ if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) endfunction() endif() +# This function may be called as a finalizer in qt6_finalize_executable() for any +# target that links against the Qml library for a shared Qt. +function(_qt_internal_generate_deploy_qml_imports_script target) + if(NOT QT6_IS_SHARED_LIBS_BUILD) + return() + endif() + get_target_property(target_type ${target} TYPE) + # TODO: Handle Android where executables are module libraries instead + if(NOT target_type STREQUAL "EXECUTABLE") + return() + endif() + + # Protect against being called multiple times in case we are being called + # explicitly before the finalizer is invoked. + get_target_property(already_generated ${target} _QT_QML_PLUGIN_SCAN_GENERATED) + get_target_property(no_import_scan ${target} QT_QML_MODULE_NO_IMPORT_SCAN) + if(already_generated OR no_import_scan) + return() + endif() + set_target_properties(${target} PROPERTIES _QT_QML_PLUGIN_SCAN_GENERATED TRUE) + + # Defer actually running qmlimportscanner until build time. This keeps the + # configure step fast and takes advantage of the build step supporting + # parallel execution if there are multiple targets that need scanning. + _qt_internal_scan_qml_imports(${target} imports_file BUILD_PHASE) + + set(is_bundle FALSE) + if(APPLE) + if(IOS) + message(FATAL_ERROR "Install support not available for iOS builds") + endif() + get_target_property(is_bundle ${target} MACOSX_BUNDLE) + endif() + set(is_bundle "$<BOOL:${is_bundle}>") + + # For macOS app bundles, the directory layout must conform to Apple's + # requirements, so we hard-code the required structure. This assumes the + # app bundle is installed to the base dir with an install command like: + # install(TARGETS ${target} BUNDLE DESTINATION .) + set(bundle_qml_dir "$<TARGET_FILE_NAME:${target}>.app/Contents/Resources/qml") + set(bundle_plugins_dir "$<TARGET_FILE_NAME:${target}>.app/Contents/PlugIns") + + _qt_internal_get_deploy_impl_dir(deploy_impl_dir) + string(MAKE_C_IDENTIFIER "${target}" target_id) + set(filename "${deploy_impl_dir}/deploy_qml_imports/${target_id}") + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + string(APPEND filename "-$<CONFIG>") + endif() + string(APPEND filename ".cmake") + + # TODO: Fix macOS multi-config bundles to work. + file(GENERATE OUTPUT "${filename}" CONTENT +"# Auto-generated deploy QML imports script for target \"${target}\". +# Do not edit, all changes will be lost. +# This file should only be included by qt_deploy_qml_imports(). + +set(__qt_opts $<${is_bundle}:BUNDLE>) +if(arg_NO_QT_IMPORTS) + list(APPEND __qt_opts NO_QT_IMPORTS) +endif() + +_qt_internal_deploy_qml_imports_for_target( + \${__qt_opts} + IMPORTS_FILE \"${imports_file}\" + PLUGINS_FOUND __qt_internal_plugins_found + QML_DIR \"$<IF:${is_bundle},${bundle_qml_dir},\${arg_QML_DIR}>\" + PLUGINS_DIR \"$<IF:${is_bundle},${bundle_plugins_dir},\${arg_PLUGINS_DIR}>\" +) + +if(arg_PLUGINS_FOUND) + set(\${arg_PLUGINS_FOUND} \"\${__qt_internal_plugins_found}\" PARENT_SCOPE) +endif() +") + +endfunction() + +# This function is currently in Technical Preview. +# Its signature and behavior might change. +function(qt6_generate_deploy_qml_app_script) + # We take the target using a TARGET keyword instead of as the first + # positional argument so that we have a consistent signature with the + # qt6_generate_deploy_app_script() from qtbase. That function might accept + # an executable instead of a target in the future, but we can't because we + # need information associated with the target (scanning all its .qml files + # for imported QML modules). + set(no_value_options + NO_UNSUPPORTED_PLATFORM_ERROR + MACOS_BUNDLE_POST_BUILD + DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM + ) + set(single_value_options + TARGET + FILENAME_VARIABLE + ) + set(multi_value_options "") + cmake_parse_arguments(PARSE_ARGV 0 arg + "${no_value_options}" "${single_value_options}" "${multi_value_options}" + ) + if(arg_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unexpected arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + if(NOT arg_TARGET) + message(FATAL_ERROR "TARGET must be specified") + endif() + if(NOT arg_FILENAME_VARIABLE) + message(FATAL_ERROR "FILENAME_VARIABLE must be specified") + endif() + + # Create a file name that will be unique for this target and the combination + # of arguments passed to this command. This allows the project to call us + # multiple times with different arguments for the same target (e.g. to + # create deployment scripts for different scenarios). + string(MAKE_C_IDENTIFIER "${arg_TARGET}" target_id) + string(SHA1 args_hash "${ARGV}") + string(SUBSTRING "${args_hash}" 0 10 short_hash) + _qt_internal_get_deploy_impl_dir(deploy_impl_dir) + set(file_name "${deploy_impl_dir}/deploy_qml_app_${target_id}_${short_hash}") + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + string(APPEND file_name "-$<CONFIG>") + endif() + set(${arg_FILENAME_VARIABLE} "${file_name}" PARENT_SCOPE) + + # This will be changed to TRUE in some future Qt version, when + # qt_deploy_runtime_dependencies can handle Linux. + set(desktop_linux_runtime_libs_deployment_supported FALSE) + + if(QT6_IS_SHARED_LIBS_BUILD) + set(qt_build_type_string "shared Qt libs") + else() + set(qt_build_type_string "static Qt libs") + endif() + + if(APPLE AND NOT IOS AND QT6_IS_SHARED_LIBS_BUILD) + # TODO: Handle non-bundle applications if possible. + get_target_property(is_bundle ${arg_TARGET} MACOSX_BUNDLE) + if(NOT is_bundle) + message(FATAL_ERROR + "Executable targets have to be app bundles to use this command " + "on Apple platforms." + ) + endif() + + file(GENERATE OUTPUT "${file_name}" CONTENT " +include(${QT_DEPLOY_SUPPORT}) +qt_deploy_qml_imports(TARGET ${arg_TARGET} PLUGINS_FOUND plugins_found) +if(NOT DEFINED __QT_DEPLOY_POST_BUILD) + qt_deploy_runtime_dependencies( + EXECUTABLE $<TARGET_FILE_NAME:${arg_TARGET}>.app + MACOS_BUNDLE + ADDITIONAL_MODULES \${plugins_found} + ) +endif()") + if(arg_MACOS_BUNDLE_POST_BUILD) + # We must not deploy the runtime dependencies, otherwise we interfere + # with CMake's RPATH rewriting at install time. We only need the QML + # imports deployed to the bundle anyway, the build RPATHs will allow + # the regular libraries, frameworks and non-QML plugins to still be + # found, even if they are outside the app bundle. + add_custom_command(TARGET ${arg_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -D "QT_DEPLOY_PREFIX=$<TARGET_PROPERTY:${arg_TARGET},BINARY_DIR>" + -D "__QT_DEPLOY_IMPL_DIR=${deploy_impl_dir}" + -D "__QT_DEPLOY_POST_BUILD=TRUE" + -P "${file_name}" + ) + endif() + + elseif(WIN32 AND QT6_IS_SHARED_LIBS_BUILD) + file(GENERATE OUTPUT "${file_name}" CONTENT " +include(${QT_DEPLOY_SUPPORT}) +qt_deploy_qml_imports(TARGET ${arg_TARGET} PLUGINS_FOUND plugins_found) +qt_deploy_runtime_dependencies( + EXECUTABLE ${CMAKE_INSTALL_BINDIR}/$<TARGET_FILE_NAME:${arg_TARGET}> + ADDITIONAL_MODULES \${plugins_found} + GENERATE_QT_CONF +)") + elseif(LINUX AND NOT CMAKE_CROSSCOMPILING AND desktop_linux_runtime_libs_deployment_supported) + # TODO: This branch will only be enabled once qt_deploy_runtime_dependencies can handle + # desktop Linux. + file(GENERATE OUTPUT "${file_name}" CONTENT " +include(${QT_DEPLOY_SUPPORT}) +qt_deploy_qml_imports(TARGET ${arg_TARGET} PLUGINS_FOUND plugins_found) +qt_deploy_runtime_dependencies( +EXECUTABLE ${CMAKE_INSTALL_BINDIR}/$<TARGET_FILE_NAME:${arg_TARGET}> +ADDITIONAL_MODULES \${plugins_found} +GENERATE_QT_CONF +)") + elseif((arg_NO_UNSUPPORTED_PLATFORM_ERROR OR + QT_INTERNAL_NO_UNSUPPORTED_PLATFORM_ERROR) + AND (arg_DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM + OR QT_INTERNAL_DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM) + AND QT6_IS_SHARED_LIBS_BUILD) + # User project explicitly requested to deploy only user QML modules on a shared Qt libs + # platform where qt_deploy_runtime_dependencies does not work. + # This is useful for projects that will deploy the Qt QML and runtime libraries manually. + # This also offers a migration path to enable qt_deploy_runtime_dependencies for + # unsupported platforms without breaking projects that already handle runtime libs manually. + # But for it to work cleanly, projects will have to enable both + # NO_UNSUPPORTED_PLATFORM_ERROR and DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM + # conditionally per platform. + file(GENERATE OUTPUT "${file_name}" CONTENT " +include(${QT_DEPLOY_SUPPORT}) +_qt_internal_show_skip_runtime_deploy_message(\"${qt_build_type_string}\") +qt_deploy_qml_imports(TARGET ${arg_TARGET} NO_QT_IMPORTS) +") + elseif(NOT arg_NO_UNSUPPORTED_PLATFORM_ERROR AND NOT QT_INTERNAL_NO_UNSUPPORTED_PLATFORM_ERROR) + # Currently we don't deploy runtime dependencies if cross-compiling or using a static Qt. + # We also don't do it if targeting Linux, but we could provide an option to do + # so if we had a deploy tool or purely CMake-based deploy implementation. + # Error out by default unless the project opted out of the error. + # This provides us a migration path in the future without breaking compatibility promises. + message(FATAL_ERROR + "Support for installing runtime dependencies is not implemented for " + "this target platform (${CMAKE_SYSTEM_NAME}, ${qt_build_type_string})." + ) + else() + file(GENERATE OUTPUT "${file_name}" CONTENT " +include(${QT_DEPLOY_SUPPORT}) +_qt_internal_show_skip_runtime_deploy_message(\"${qt_build_type_string}\") +_qt_internal_show_skip_qml_runtime_deploy_message() +") + endif() + +endfunction() + +if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) + macro(qt_generate_deploy_qml_app_script) + qt6_generate_deploy_qml_app_script(${ARGV}) + endmacro() +endif() + + function(_qt_internal_add_static_qml_plugin_dependencies plugin_target backing_target) # Protect against multiple calls of qt_add_qml_plugin. get_target_property(plugin_deps_added "${plugin_target}" _qt_extra_static_qml_plugin_deps_added) diff --git a/tools/qmlimportscanner/main.cpp b/tools/qmlimportscanner/main.cpp index b48ab4eeec..85542cbba8 100644 --- a/tools/qmlimportscanner/main.cpp +++ b/tools/qmlimportscanner/main.cpp @@ -648,6 +648,7 @@ int main(int argc, char *argv[]) QStringList qmlImportPaths; QStringList qrcFiles; bool generateCmakeContent = false; + QString outputFile; int i = 1; while (i < args.count()) { @@ -676,6 +677,14 @@ int main(int argc, char *argv[]) generateCmakeContent = true; } else if (arg == QLatin1String("-qrcFiles")) { argReceiver = &qrcFiles; + } else if (arg == QLatin1String("-output-file")) { + if (i >= args.count()) { + std::cerr << "-output-file requires an argument\n"; + return 1; + } + outputFile = args.at(i); + ++i; + continue; } else { std::cerr << qPrintable(appName) << ": Invalid argument: \"" << qPrintable(arg) << "\"\n"; @@ -716,6 +725,17 @@ int main(int argc, char *argv[]) content = QJsonDocument(QJsonArray::fromVariantList(imports)).toJson(); } - std::cout << content.constData() << std::endl; + if (outputFile.isEmpty()) { + std::cout << content.constData() << std::endl; + } else { + QFile f(outputFile); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + std::cerr << qPrintable(appName) << ": Unable to write to output file: \"" + << qPrintable(outputFile) << "\"\n"; + return 1; + } + QTextStream out(&f); + out << content << "\n"; + } return 0; } |