From 561fc8107f38b93808343e35c62d7d06704f8eb6 Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Thu, 20 May 2021 13:38:30 +0200 Subject: [PATCH] CMake: Allow promoting the Qt libraries to be global targets User projects can set the QT_PROMOTE_TO_GLOBAL_TARGETS variable to true so that the various imported targets created by find_package(Qt6) are promoted to global targets. This would allow a project to find Qt packages in a subdirectory scope while using those Qt targets from a different scope. E.g. it fixes errors like CMake Error at CMakeLists.txt:5 (target_link_libraries): Error evaluating generator expression: $ Objects of target "Qt6::Widgets_resources_1" referenced but no such target exists. when trying to use a static Qt from a sibling scope. Various 3rd party dependency targets (like Atomic or ZLIB) are not made global due to limitations in CMake, but as long as those targets are not mentioned directly, it shouldn't cause issues. The targets are made global in the generated QtFooAdditionalTargetInfo.cmake file. To ensure that resource object libraries promoted, the generation of the file has to be done at the end of the defining scope where qt_internal_export_additional_targets_file is called, which is achieved with a deferred finalizer. Replaced all occurrences of target promotion with a helper function which allows tracing of all promoted targets by specifying --log-level=debug to CMake. Pick-to: 6.2 Fixes: QTBUG-92878 Change-Id: Ic4ec03b0bc383d7e591a58c520c3974fbea746d2 Reviewed-by: Alexey Edelev Reviewed-by: Qt CI Bot Reviewed-by: Joerg Bornemann --- cmake/QtFindPackageHelpers.cmake | 3 +- cmake/QtPluginHelpers.cmake | 2 +- cmake/QtPublicTargetHelpers.cmake | 14 ++ cmake/QtPublicWalkLibsHelpers.cmake | 5 +- cmake/QtResourceHelpers.cmake | 4 + cmake/QtScopeFinalizerHelpers.cmake | 3 + cmake/QtTargetHelpers.cmake | 135 +++++++++++++++++- cmake/QtToolHelpers.cmake | 5 +- tests/auto/cmake/CMakeLists.txt | 4 + .../test_global_promotion/CMakeLists.txt | 13 ++ .../subdir_with_global_qt/CMakeLists.txt | 14 ++ .../subdir_with_local_qt/CMakeLists.txt | 12 ++ 12 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 tests/auto/cmake/test_global_promotion/CMakeLists.txt create mode 100644 tests/auto/cmake/test_global_promotion/subdir_with_global_qt/CMakeLists.txt create mode 100644 tests/auto/cmake/test_global_promotion/subdir_with_local_qt/CMakeLists.txt diff --git a/cmake/QtFindPackageHelpers.cmake b/cmake/QtFindPackageHelpers.cmake index 1853240f22..fca039549e 100644 --- a/cmake/QtFindPackageHelpers.cmake +++ b/cmake/QtFindPackageHelpers.cmake @@ -159,8 +159,7 @@ macro(qt_find_package) qt_internal_should_not_promote_package_target_to_global( "${qt_find_package_target_name}" should_not_promote) if(NOT is_global AND NOT should_not_promote) - set_property(TARGET ${qt_find_package_target_name} PROPERTY - IMPORTED_GLOBAL TRUE) + __qt_internal_promote_target_to_global(${qt_find_package_target_name}) qt_find_package_promote_targets_to_global_scope( "${qt_find_package_target_name}") endif() diff --git a/cmake/QtPluginHelpers.cmake b/cmake/QtPluginHelpers.cmake index c4e0df4415..a7d2720ee4 100644 --- a/cmake/QtPluginHelpers.cmake +++ b/cmake/QtPluginHelpers.cmake @@ -343,7 +343,7 @@ function(qt_internal_add_plugin target) qt_path_join(config_install_dir ${QT_CONFIG_INSTALL_DIR} ${path_suffix}) qt_internal_export_additional_targets_file( - TARGETS ${target} + TARGETS ${target} ${plugin_init_target} EXPORT_NAME_PREFIX ${INSTALL_CMAKE_NAMESPACE}${target} CONFIG_INSTALL_DIR "${config_install_dir}") diff --git a/cmake/QtPublicTargetHelpers.cmake b/cmake/QtPublicTargetHelpers.cmake index 3804b18ac4..70e6c85b1a 100644 --- a/cmake/QtPublicTargetHelpers.cmake +++ b/cmake/QtPublicTargetHelpers.cmake @@ -114,3 +114,17 @@ function(__qt_internal_collect_resource_objects_recursively out_var target initi endforeach() set(${out_var} "${resource_targets}" PARENT_SCOPE) endfunction() + +function(__qt_internal_promote_target_to_global target) + get_property(is_global TARGET ${target} PROPERTY IMPORTED_GLOBAL) + if(NOT is_global) + message(DEBUG "Promoting target to global: '${target}'") + set_property(TARGET ${target} PROPERTY IMPORTED_GLOBAL TRUE) + endif() +endfunction() + +function(__qt_internal_promote_target_to_global_checked target) + if(QT_PROMOTE_TO_GLOBAL_TARGETS) + __qt_internal_promote_target_to_global(${target}) + endif() +endfunction() diff --git a/cmake/QtPublicWalkLibsHelpers.cmake b/cmake/QtPublicWalkLibsHelpers.cmake index 963c15d4c6..19eef91356 100644 --- a/cmake/QtPublicWalkLibsHelpers.cmake +++ b/cmake/QtPublicWalkLibsHelpers.cmake @@ -222,14 +222,13 @@ function(__qt_internal_walk_libs endif() get_property(is_imported TARGET ${lib_target_unaliased} PROPERTY IMPORTED) - get_property(is_global TARGET ${lib_target_unaliased} PROPERTY IMPORTED_GLOBAL) # Allow opting out of promotion. This is useful in certain corner cases # like with WrapLibClang and Threads in qttools. qt_internal_should_not_promote_package_target_to_global( "${lib_target_unaliased}" should_not_promote) - if(NOT is_global AND is_imported AND NOT should_not_promote) - set_property(TARGET ${lib_target_unaliased} PROPERTY IMPORTED_GLOBAL TRUE) + if(is_imported AND NOT should_not_promote) + __qt_internal_promote_target_to_global(${lib_target_unaliased}) endif() endif() elseif("${lib_target}" MATCHES "^Qt::(.*)") diff --git a/cmake/QtResourceHelpers.cmake b/cmake/QtResourceHelpers.cmake index 3c719d1dd0..4b7d81cc7f 100644 --- a/cmake/QtResourceHelpers.cmake +++ b/cmake/QtResourceHelpers.cmake @@ -21,6 +21,10 @@ function(qt_internal_add_resource target resourceName) EXPORT "${INSTALL_CMAKE_NAMESPACE}${target}Targets" DESTINATION "${INSTALL_LIBDIR}" ) + qt_internal_add_targets_to_additional_targets_export_file( + TARGETS ${out_targets} + EXPORT_NAME_PREFIX "${INSTALL_CMAKE_NAMESPACE}${target}" + ) qt_internal_record_rcc_object_files("${target}" "${out_targets}" INSTALL_DIRECTORY "${INSTALL_LIBDIR}") diff --git a/cmake/QtScopeFinalizerHelpers.cmake b/cmake/QtScopeFinalizerHelpers.cmake index 59e89cc9e2..6dae635ba2 100644 --- a/cmake/QtScopeFinalizerHelpers.cmake +++ b/cmake/QtScopeFinalizerHelpers.cmake @@ -74,6 +74,9 @@ function(qt_watch_current_list_dir variable access value current_list_file stack qt_finalize_plugin(${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) elseif(func STREQUAL "qt_internal_finalize_app") qt_internal_finalize_app(${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) + elseif(func STREQUAL "qt_internal_export_additional_targets_file_finalizer") + qt_internal_export_additional_targets_file_finalizer( + ${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) else() message(FATAL_ERROR "qt_watch_current_list_dir doesn't know about ${func}. Consider adding it.") endif() diff --git a/cmake/QtTargetHelpers.cmake b/cmake/QtTargetHelpers.cmake index cc8bfde85f..be1cf5e2da 100644 --- a/cmake/QtTargetHelpers.cmake +++ b/cmake/QtTargetHelpers.cmake @@ -264,9 +264,26 @@ function(qt_internal_check_directory_or_type name dir type default result_var) endif() endfunction() +macro(qt_internal_get_export_additional_targets_keywords option_args single_args multi_args) + set(${option_args} + ) + set(${single_args} + EXPORT_NAME_PREFIX + ) + set(${multi_args} + TARGETS + TARGET_EXPORT_NAMES + ) +endmacro() + # Create a Qt*AdditionalTargetInfo.cmake file that is included by Qt*Config.cmake # and sets IMPORTED_*_ properties on the exported targets. # +# The file also makes the targets global if the QT_PROMOTE_TO_GLOBAL_TARGETS property is set in the +# consuming project. +# Only the specified TARGETS are made global. Transitive 3rd party targets are not made global, due +# to limitations in CMake. See https://gitlab.kitware.com/cmake/cmake/-/issues/22291 +# # EXPORT_NAME_PREFIX: # The portion of the file name before AdditionalTargetInfo.cmake # CONFIG_INSTALL_DIR: @@ -283,14 +300,75 @@ endfunction() # TARGET_EXPORT_NAMES = Qt6::qmljs # function(qt_internal_export_additional_targets_file) - cmake_parse_arguments(arg "" "EXPORT_NAME_PREFIX;CONFIG_INSTALL_DIR" - "TARGETS;TARGET_EXPORT_NAMES" ${ARGN}) + qt_internal_get_export_additional_targets_keywords(option_args single_args multi_args) + cmake_parse_arguments(arg + "${option_args}" + "${single_args};CONFIG_INSTALL_DIR" + "${multi_args}" + ${ARGN}) + + qt_internal_append_export_additional_targets() + + set_property(GLOBAL APPEND PROPERTY _qt_export_additional_targets_ids "${id}") + set_property(GLOBAL APPEND + PROPERTY _qt_export_additional_targets_export_name_prefix_${id} "${arg_EXPORT_NAME_PREFIX}") + set_property(GLOBAL APPEND + PROPERTY _qt_export_additional_targets_config_install_dir_${id} "${arg_CONFIG_INSTALL_DIR}") + + qt_add_list_file_finalizer(qt_internal_export_additional_targets_file_finalizer) +endfunction() + +function(qt_internal_get_export_additional_targets_id export_name out_var) + string(MAKE_C_IDENTIFIER "${export_name}" id) + set(${out_var} "${id}" PARENT_SCOPE) +endfunction() + +# Uses outer-scope variables to keep the implementation less verbose. +macro(qt_internal_append_export_additional_targets) + qt_internal_validate_export_additional_targets( + EXPORT_NAME_PREFIX "${arg_EXPORT_NAME_PREFIX}" + TARGETS ${arg_TARGETS} + TARGET_EXPORT_NAMES ${arg_TARGET_EXPORT_NAMES}) + + qt_internal_get_export_additional_targets_id("${arg_EXPORT_NAME_PREFIX}" id) + + set_property(GLOBAL APPEND + PROPERTY _qt_export_additional_targets_${id} "${arg_TARGETS}") + set_property(GLOBAL APPEND + PROPERTY _qt_export_additional_target_export_names_${id} "${arg_TARGET_EXPORT_NAMES}") +endmacro() + +# Can be called to add additional targets to the file after the initial setup call. +# Used for resources. +function(qt_internal_add_targets_to_additional_targets_export_file) + qt_internal_get_export_additional_targets_keywords(option_args single_args multi_args) + cmake_parse_arguments(arg + "${option_args}" + "${single_args}" + "${multi_args}" + ${ARGN}) + + qt_internal_append_export_additional_targets() +endfunction() + +function(qt_internal_validate_export_additional_targets) + qt_internal_get_export_additional_targets_keywords(option_args single_args multi_args) + cmake_parse_arguments(arg + "${option_args}" + "${single_args}" + "${multi_args}" + ${ARGN}) + + if(NOT arg_EXPORT_NAME_PREFIX) + message(FATAL_ERROR "qt_internal_validate_export_additional_targets: " + "Missing EXPORT_NAME_PREFIX argument.") + endif() list(LENGTH arg_TARGETS num_TARGETS) list(LENGTH arg_TARGET_EXPORT_NAMES num_TARGET_EXPORT_NAMES) if(num_TARGET_EXPORT_NAMES GREATER 0) if(NOT num_TARGETS EQUAL num_TARGET_EXPORT_NAMES) - message(FATAL_ERROR "qt_internal_export_additional_targets_file: " + message(FATAL_ERROR "qt_internal_validate_export_additional_targets: " "TARGET_EXPORT_NAMES is set but has ${num_TARGET_EXPORT_NAMES} elements while " "TARGETS has ${num_TARGETS} elements. " "They must contain the same number of elements.") @@ -299,6 +377,34 @@ function(qt_internal_export_additional_targets_file) set(arg_TARGET_EXPORT_NAMES ${arg_TARGETS}) endif() + set(arg_TARGETS "${arg_TARGETS}" PARENT_SCOPE) + set(arg_TARGET_EXPORT_NAMES "${arg_TARGET_EXPORT_NAMES}" PARENT_SCOPE) +endfunction() + +# The finalizer might be called multiple times in the same scope, but only the first one will +# process all the ids. +function(qt_internal_export_additional_targets_file_finalizer) + get_property(ids GLOBAL PROPERTY _qt_export_additional_targets_ids) + + foreach(id ${ids}) + qt_internal_export_additional_targets_file_handler("${id}") + endforeach() + + set_property(GLOBAL PROPERTY _qt_export_additional_targets_ids "") +endfunction() + +function(qt_internal_export_additional_targets_file_handler id) + get_property(arg_EXPORT_NAME_PREFIX GLOBAL PROPERTY + _qt_export_additional_targets_export_name_prefix_${id}) + get_property(arg_CONFIG_INSTALL_DIR GLOBAL PROPERTY + _qt_export_additional_targets_config_install_dir_${id}) + get_property(arg_TARGETS GLOBAL PROPERTY + _qt_export_additional_targets_${id}) + get_property(arg_TARGET_EXPORT_NAMES GLOBAL PROPERTY + _qt_export_additional_target_export_names_${id}) + + list(LENGTH arg_TARGETS num_TARGETS) + # Determine the release configurations we're currently building if(QT_GENERATOR_IS_MULTI_CONFIG) set(active_configurations ${CMAKE_CONFIGURATION_TYPES}) @@ -337,18 +443,33 @@ if(NOT DEFINED QT_DEFAULT_IMPORT_CONFIGURATION) set(QT_DEFAULT_IMPORT_CONFIGURATION ${uc_default_cfg}) endif() ") + math(EXPR n "${num_TARGETS} - 1") foreach(i RANGE ${n}) list(GET arg_TARGETS ${i} target) list(GET arg_TARGET_EXPORT_NAMES ${i} target_export_name) - get_target_property(target_type ${target} TYPE) - if(target_type STREQUAL "INTERFACE_LIBRARY") - continue() - endif() + set(full_target ${target_export_name}) if(NOT full_target MATCHES "^${QT_CMAKE_EXPORT_NAMESPACE}::") string(PREPEND full_target "${QT_CMAKE_EXPORT_NAMESPACE}::") endif() + + # Tools are already made global unconditionally in QtFooToolsConfig.cmake. + # And the + get_target_property(target_type ${target} TYPE) + if(NOT target_type STREQUAL "EXECUTABLE") + string(APPEND content + "__qt_internal_promote_target_to_global_checked(${full_target})\n") + endif() + + # INTERFACE libraries don't have IMPORTED_LOCATION-like properties. + # OBJECT libraries have properties like IMPORTED_OBJECTS instead. + # Skip the rest of the procesing for those. + if(target_type STREQUAL "INTERFACE_LIBRARY" OR target_type STREQUAL "OBJECT_LIBRARY") + continue() + endif() + + # FIXME: Don't add IMPORTED_SOLIB and IMPORTED_SONAME properties for executables. set(properties_retrieved TRUE) if(NOT "${uc_release_cfg}" STREQUAL "") string(APPEND content "get_target_property(_qt_imported_location ${full_target} IMPORTED_LOCATION_${uc_release_cfg})\n") diff --git a/cmake/QtToolHelpers.cmake b/cmake/QtToolHelpers.cmake index 24ab9e0162..4274e622a4 100644 --- a/cmake/QtToolHelpers.cmake +++ b/cmake/QtToolHelpers.cmake @@ -321,10 +321,7 @@ function(qt_export_tools module_name) endif() set(extra_cmake_statements "${extra_cmake_statements} if (NOT QT_NO_CREATE_TARGETS) - get_property(is_global TARGET ${INSTALL_CMAKE_NAMESPACE}::${tool_name} PROPERTY IMPORTED_GLOBAL) - if(NOT is_global) - set_property(TARGET ${INSTALL_CMAKE_NAMESPACE}::${tool_name} PROPERTY IMPORTED_GLOBAL TRUE) - endif() + __qt_internal_promote_target_to_global(${INSTALL_CMAKE_NAMESPACE}::${tool_name}) endif() ") list(APPEND tool_targets "${QT_CMAKE_EXPORT_NAMESPACE}::${tool_name}") diff --git a/tests/auto/cmake/CMakeLists.txt b/tests/auto/cmake/CMakeLists.txt index b95c530640..8852c7e68b 100644 --- a/tests/auto/cmake/CMakeLists.txt +++ b/tests/auto/cmake/CMakeLists.txt @@ -247,6 +247,10 @@ set_tests_properties(test_import_plugins PROPERTIES FIXTURES_REQUIRED build_mock _qt_internal_test_expect_pass(test_versionless_targets) +if(NOT NO_GUI) + _qt_internal_test_expect_pass(test_global_promotion) +endif() + _qt_internal_test_expect_pass(test_add_resources_binary_generated BINARY test_add_resources_binary_generated) diff --git a/tests/auto/cmake/test_global_promotion/CMakeLists.txt b/tests/auto/cmake/test_global_promotion/CMakeLists.txt new file mode 100644 index 0000000000..c59dece81e --- /dev/null +++ b/tests/auto/cmake/test_global_promotion/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.14) + +project(global_promotion) + +add_subdirectory(subdir_with_local_qt) +add_subdirectory(subdir_with_global_qt) + +set(file_path "${CMAKE_CURRENT_BINARY_DIR}/main.cpp") +file(GENERATE OUTPUT "${file_path}" CONTENT "int main() { return 0; }") +add_executable(exe main.cpp) + +# The Qt targets found in the 2nd child directory scope should be available in this scope. +target_link_libraries(exe PRIVATE lib_global_qt) diff --git a/tests/auto/cmake/test_global_promotion/subdir_with_global_qt/CMakeLists.txt b/tests/auto/cmake/test_global_promotion/subdir_with_global_qt/CMakeLists.txt new file mode 100644 index 0000000000..a8808a8c83 --- /dev/null +++ b/tests/auto/cmake/test_global_promotion/subdir_with_global_qt/CMakeLists.txt @@ -0,0 +1,14 @@ +message(STATUS "Entered subdir_with_global_qt subdirectory") + +set(file_path "${CMAKE_CURRENT_BINARY_DIR}/lib.cpp") +file(GENERATE OUTPUT "${file_path}" CONTENT "int foo() { return 42; }") +add_library(lib_global_qt STATIC "${file_path}") + +# These Qt targets will be available in all scopes of the project. +# The previous local targets are simply shadowed. +set(QT_PROMOTE_TO_GLOBAL_TARGETS ON) +find_package(Qt6 REQUIRED COMPONENTS Gui) + +target_link_libraries(lib_global_qt PRIVATE Qt6::Gui) + +message(STATUS "Exiting subdir_with_global_qt subdirectory") diff --git a/tests/auto/cmake/test_global_promotion/subdir_with_local_qt/CMakeLists.txt b/tests/auto/cmake/test_global_promotion/subdir_with_local_qt/CMakeLists.txt new file mode 100644 index 0000000000..1bed5a2f99 --- /dev/null +++ b/tests/auto/cmake/test_global_promotion/subdir_with_local_qt/CMakeLists.txt @@ -0,0 +1,12 @@ +message(STATUS "Entered subdir_with_local_qt subdirectory") + +set(file_path "${CMAKE_CURRENT_BINARY_DIR}/lib.cpp") +file(GENERATE OUTPUT "${file_path}" CONTENT "int foo() { return 42; }") +add_library(lib_local_qt STATIC "${file_path}") + +# These Qt targets will be local to this directory scope. +find_package(Qt6 REQUIRED COMPONENTS Gui) + +target_link_libraries(lib_local_qt PRIVATE Qt6::Gui) + +message(STATUS "Exiting subdir_with_local_qt subdirectory")