diff --git a/cmake/QtBaseHelpers.cmake b/cmake/QtBaseHelpers.cmake index bd59bff8026..632cddc587f 100644 --- a/cmake/QtBaseHelpers.cmake +++ b/cmake/QtBaseHelpers.cmake @@ -97,6 +97,8 @@ macro(qt_internal_qtbase_pre_project_setup) include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/QtPublicCMakeVersionHelpers.cmake") qt_internal_check_and_warn_about_unsuitable_cmake_version() + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/QtPublicCMakeEarlyPolicyHelpers.cmake") + ## Add some paths to check for cmake modules: list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" diff --git a/cmake/QtBuildHelpers.cmake b/cmake/QtBuildHelpers.cmake index 941b6d74ef1..c1e59036aba 100644 --- a/cmake/QtBuildHelpers.cmake +++ b/cmake/QtBuildHelpers.cmake @@ -321,6 +321,7 @@ function(qt_internal_get_qt_build_public_files_to_install out_var) set(${out_var} QtCopyFileIfDifferent.cmake QtInitProject.cmake + QtPublicCMakeEarlyPolicyHelpers.cmake # Public CMake files that are installed next Qt6Config.cmake, but are NOT included by it. # Instead they are included by the generated CMake toolchain file. diff --git a/cmake/QtConfig.cmake.in b/cmake/QtConfig.cmake.in index d1ffc7456b9..2c34af97713 100644 --- a/cmake/QtConfig.cmake.in +++ b/cmake/QtConfig.cmake.in @@ -3,6 +3,10 @@ @PACKAGE_INIT@ +# This is included before the cmake_minimum_required on purpose. +include("${CMAKE_CURRENT_LIST_DIR}/QtPublicCMakeEarlyPolicyHelpers.cmake") +__qt_internal_save_directory_scope_policy_cmp0156() + cmake_minimum_required(VERSION @min_new_policy_version@...@max_new_policy_version@) include("${CMAKE_CURRENT_LIST_DIR}/@INSTALL_CMAKE_NAMESPACE@ConfigExtras.cmake") diff --git a/cmake/QtPublicCMakeEarlyPolicyHelpers.cmake b/cmake/QtPublicCMakeEarlyPolicyHelpers.cmake new file mode 100644 index 00000000000..fe71eed7d1f --- /dev/null +++ b/cmake/QtPublicCMakeEarlyPolicyHelpers.cmake @@ -0,0 +1,24 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Save the current value of the CMP0156 policy in a propert of the current directory scope. +function(__qt_internal_save_directory_scope_policy_cmp0156) + if(NOT POLICY CMP0156) + return() + endif() + + # Exit early if we already saved the policy value for this directory scope. + get_property(policy_value_set DIRECTORY PROPERTY _qt_internal_policy_cmp0156_value_set) + if(policy_value_set) + return() + endif() + + cmake_policy(GET CMP0156 policy_value) + set_property(DIRECTORY PROPERTY _qt_internal_policy_cmp0156_value "${policy_value}") + set_property(DIRECTORY PROPERTY _qt_internal_policy_cmp0156_value_set "TRUE") +endfunction() + +function(__qt_internal_get_directory_scope_policy_cmp0156 out_var) + get_property(policy_value DIRECTORY PROPERTY _qt_internal_policy_cmp0156_value) + set(${out_var} "${policy_value}" PARENT_SCOPE) +endfunction() diff --git a/cmake/QtPublicCMakeVersionHelpers.cmake b/cmake/QtPublicCMakeVersionHelpers.cmake index 58c7aa80417..316a69af677 100644 --- a/cmake/QtPublicCMakeVersionHelpers.cmake +++ b/cmake/QtPublicCMakeVersionHelpers.cmake @@ -104,21 +104,78 @@ function(__qt_internal_require_suitable_cmake_version_for_using_qt) endif() endfunction() +# Handle force-assignment of CMP0156 policy when using CMake 3.29+. +# +# For Apple-platforms we set it to NEW, to avoid duplicate linker issues when using -ObjC flag. +# +# For non-Apple platforms we set it to OLD, because we haven't done the necessary testing to +# see which platforms / linkers can handle the new deduplication behavior, without breaking the +# various linking techniques that Qt uses for object library propagation. function(__qt_internal_set_cmp0156) - if(POLICY CMP0156) - if(QT_FORCE_CMP0156_TO_NEW) - cmake_policy(SET CMP0156 "NEW") - else() - cmake_policy(GET CMP0156 policy_value) - if(NOT "${policy_value}" STREQUAL "OLD") - if("${policy_value}" STREQUAL "NEW" AND NOT QT_BUILDING_QT) - message(WARNING "CMP0156 is set to '${policy_value}'. Qt forces the 'OLD'" - " behavior of this policy by default. Set QT_FORCE_CMP0156_TO_NEW=ON to" - " force the 'NEW' behavior for the Qt commands that create either" - " library or executable targets.") - endif() - cmake_policy(SET CMP0156 "OLD") - endif() - endif() + # Exit early if not using CMake 3.29+ + if(NOT POLICY CMP0156) + return() + endif() + + # Honor this variable if it's set and TRUE. It was previously introduced to allow working around + # the forced OLD value. + if(QT_FORCE_CMP0156_TO_NEW) + cmake_policy(SET CMP0156 "NEW") + message(DEBUG "Force setting the CMP0156 policy to user provided value: NEW") + return() + endif() + + # Allow forcing to OLD / NEW or empty behavior due the default being NEW for Apple platforms. + if(QT_FORCE_CMP0156_TO_VALUE) + cmake_policy(SET CMP0156 "${QT_FORCE_CMP0156_TO_VALUE}") + message(DEBUG "Force setting the CMP0156 policy to user provided value: " + "${QT_FORCE_CMP0156_TO_VALUE}") + return() + endif() + + # Get the current value of the policy, as saved by the Qt6 package as soon as it is found. + # We can't just do cmake_policy(GET CMP0156 policy_value) here, because our Qt6Config.cmake and + # Qt6FooConfig.cmake files use cmake_minimum_required, which reset the policy value that + # might have been set by the project developer or the user. + # And a function uses the policy values that were set when the function was defined, not when + # it is called. + __qt_internal_get_directory_scope_policy_cmp0156(policy_value) + + # Apple linkers (legacy Apple ld64, as well as the new ld-prime) don't care about the + # link line order when linking static libraries, compared to Linux GNU ld. + # But they care about duplicate static frameworks (not libraries) when used in conjunction with + # the -ObjC flag, which force loads all static libraries / frameworks that contain Objective-C + # categories or classes. This can cause duplicate symbol errors. + # To avoid the issue, we want to enable the policy, so we de-duplicate the libraries. + if(APPLE) + set(default_policy_value NEW) + set(unsupported_policy_value OLD) + set(platform_string "Apple") + else() + # For non-Apple linkers, we keep the previous behavior of not deduplicating libraries, + # because we haven't done the necessary testing to identify on which platforms + # it is safe to deduplicate. + set(default_policy_value OLD) + set(unsupported_policy_value NEW) + set(platform_string "non-Apple") + endif() + + # Force set the default policy value for the given platform, even if the policy value is + # the same or empty. That's because in the calling function scope, the value can be empty + # due to the cmake_minimum_required call in Qt6Config.cmake resetting the policy value. + message(DEBUG "Force setting the CMP0156 policy to '${default_policy_value}' " + "for ${platform_string} platforms.") + cmake_policy(SET CMP0156 "${default_policy_value}") + + # If the policy is explicitly set to a value other than the default, issue a warning. + # Don't show the warning if the policy is unset, which would be the default for most + # projects, because it's too much noise. Also don't show it for Qt builds. + if("${policy_value}" STREQUAL "${unsupported_policy_value}" AND NOT QT_BUILDING_QT) + message(WARNING + "CMP0156 is set to '${policy_value}'. Qt forces the '${default_policy_value}'" + " behavior of this policy for ${platform_string} platforms by default." + " Set QT_FORCE_CMP0156_TO_VALUE=${unsupported_policy_value} to force" + " the '${unsupported_policy_value}' behavior for Qt commands that create" + " library or executable targets.") endif() endfunction() diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/CMakeLists.txt b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/CMakeLists.txt new file mode 100644 index 00000000000..fa5568bebe0 --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +project(${RunCMake_TEST} LANGUAGES CXX OBJCXX) +include(${RunCMake_TEST}.cmake) diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/RunCMakeTest.cmake b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/RunCMakeTest.cmake new file mode 100644 index 00000000000..beb1710f69d --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/RunCMakeTest.cmake @@ -0,0 +1,21 @@ +include(RunCMake) + +set(cmake_opts "-DQt6_DIR=${Qt6_DIR}") + +# Check that configuration and build suceeds when a project does not set an explicit policy value. +set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/project-policy-none-build") +run_cmake_with_options(project-policy-none ${cmake_opts}) + +set(RunCMake_TEST_NO_CLEAN TRUE) +run_cmake_command(project-policy-none-build ${CMAKE_COMMAND} --build .) + +# Check that we get a warning when the project sets the policy before find_package. +set(RunCMake_TEST_NO_CLEAN FALSE) +set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/project-policy-old-build") +run_cmake_with_options(project-policy-old ${cmake_opts} -DCMP0156-OLD=TRUE) + +# Check that we get do not get a warning when the project force sets the policy to NEW. +set(RunCMake_TEST_NO_CLEAN FALSE) +set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/project-policy-new-forced-build") +run_cmake_with_options(project-policy-new-forced ${cmake_opts} -DFORCE-CMP0156-NEW=TRUE) + diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/core_helper.mm b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/core_helper.mm new file mode 100644 index 00000000000..40c41f31a0f --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/core_helper.mm @@ -0,0 +1,22 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +// We need at least one Objective-C class or category to be defined in the file, so that +// -ObjC triggers the second loading of the same static framework. +// Note the duplicate symbol errors happen when linking static frameworks, note static libraries. +// This seems to be a gotcha in both the classic ld linker and in the new ld64 ld_prime linker. + +@interface SimpleClass : NSObject +- (void)helloFunc; +@end + +@implementation SimpleClass +- (void)helloFunc +{ + return; +} +@end + +int core_helper_func() { return 0; }; diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/gui_helper.cpp b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/gui_helper.cpp new file mode 100644 index 00000000000..6035d45f5bd --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/gui_helper.cpp @@ -0,0 +1,4 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +int gui_helper_func() { return 0; }; diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/main.cpp b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/main.cpp new file mode 100644 index 00000000000..6e90d20e278 --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/main.cpp @@ -0,0 +1,13 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +int core_helper_func(); +int gui_helper_func(); + +int main(int argc, char **argv) { + QWindow w; + w.show(); + return core_helper_func() + gui_helper_func(); +} diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-common.cmake b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-common.cmake new file mode 100644 index 00000000000..3e32996c08d --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-common.cmake @@ -0,0 +1,26 @@ +if(CMP0156-OLD) + cmake_policy(SET CMP0156 OLD) +endif() +if(FORCE-CMP0156-NEW) + set(QT_FORCE_CMP0156_TO_VALUE NEW) +endif() + +find_package(Qt6 REQUIRED COMPONENTS Gui) + +qt_add_library(core_helper STATIC) +target_sources(core_helper PRIVATE core_helper.mm) +set_target_properties(core_helper PROPERTIES FRAMEWORK TRUE) + +qt_add_library(gui_helper STATIC) +target_sources(gui_helper PRIVATE gui_helper.cpp) +set_target_properties(gui_helper PROPERTIES FRAMEWORK TRUE) +target_link_libraries(gui_helper PRIVATE core_helper) + +qt_add_executable(app) +target_sources(app PRIVATE main.cpp) +target_link_libraries(app PRIVATE Qt6::Gui) +target_link_options(app PRIVATE -ObjC) + +# This will cause core_helper to be linked into the app twice if +# policy CMP0156 is not set to NEW, and this will cause duplicate symbol errors. +target_link_libraries(app PRIVATE core_helper gui_helper) diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-new-forced-stderr.txt b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-new-forced-stderr.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-new-forced.cmake b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-new-forced.cmake new file mode 100644 index 00000000000..9f2078eb3e7 --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-new-forced.cmake @@ -0,0 +1 @@ +include(project-common.cmake) diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-none.cmake b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-none.cmake new file mode 100644 index 00000000000..9f2078eb3e7 --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-none.cmake @@ -0,0 +1 @@ +include(project-common.cmake) diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-old-stderr.txt b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-old-stderr.txt new file mode 100644 index 00000000000..f7ba11b2125 --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-old-stderr.txt @@ -0,0 +1 @@ +CMP0156 is set to 'OLD' diff --git a/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-old.cmake b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-old.cmake new file mode 100644 index 00000000000..9f2078eb3e7 --- /dev/null +++ b/tests/auto/cmake/RunCMake/AppleFrameworkDeduplication/project-policy-old.cmake @@ -0,0 +1 @@ +include(project-common.cmake) diff --git a/tests/auto/cmake/RunCMake/CMakeLists.txt b/tests/auto/cmake/RunCMake/CMakeLists.txt index 313bf464214..bb826775b2d 100644 --- a/tests/auto/cmake/RunCMake/CMakeLists.txt +++ b/tests/auto/cmake/RunCMake/CMakeLists.txt @@ -16,3 +16,7 @@ if(TARGET Qt::OpenGL) list(APPEND extra_run_cmake_args "-DHAS_OPENGL=TRUE") endif() add_RunCMake_test(Qt6DirConfiguration ${extra_run_cmake_args}) + +if(APPLE AND TARGET Qt::Gui) + add_RunCMake_test(AppleFrameworkDeduplication "-DQt6_DIR=${Qt6_DIR}") +endif()