CMake: Adjust CMP0156 policy handling for Apple platforms

Backstory.

1) Starting with Qt 6.8, the prebuilt Qt for iOS SDK is built as
static framework bundles instead of static libraries. That is done so
that we can embed a privacy manifest into each framework bundle.

2) Up until CMake 3.28, CMake would not attempt to de-duplicate static
libraries (or frameworks) on the command line. Starting with CMake
3.29, the CMP0156 policy was introduced to allow such de-duplication.

3) Since a while ago, Qt had its own policy handling for CMP0156,
which it force sets to OLD, to disable any de-duplication. That was
done to avoid possible regressions on platforms where link order
matters.

4) A developer might add the -ObjC linker flag to a project, to ensure
the linker retains all Objective-C categories it encounters in all the
static libraries that were provided on the link line. Retaining in
this case means that the /whole/ object file of a static library will
be linked to the final executable.

5) The Apple ld linker (both the legacy and the new ld_prime one)
can't cope with duplicate static frameworks on the link line when the
-ObjC flag is used.
It ends up throwing duplicate symbol errors, from trying to link the
same object file from the same static framework more than once.
The linker works just fine if the link line contains duplicate static
libraries, rather than static frameworks.

6) When a project links against Qt6::Gui and Qt6::Core, the link line
will contain Qt6::Core twice. This gets even more involved, when
linking plugins, which cause static framework cycles, and thus a
framework might appear multiple times.

Thus, we have the situation that Qt forces the CMP0156 policy to OLD,
Qt6::Core appears multiple times on the link line, no de-duplication
is performed, the project adds the -ObjC flag, and the linker throws
duplicate symbol errors.

We can fix this by force setting the CMP0156 policy to NEW when
targeting Apple platforms and using CMake 3.29+.

A potential workaround for a project developer is to set the policy
to NEW manually in their project.
Unfortunately that doesn't work for older Qt versions.
That's because CMake applies the policy value when add_executable is
called, and the policy value in qt_add_executable is the one that is
recorded when the function is defined. And the recorded policy is
always OLD, because Qt6Config.cmake calls cmake_minimum_required with
VERSION up to 3.21, which resets the policy value to OLD.

So we have to force set the policy in qt_add_executable /
qt_add_library via the existing __qt_internal_set_cmp0156 function.

The __qt_internal_set_cmp0156 had some diagnostics to show a warning
when the user modifies the policy themselves, but this never worked
because of reason stated above: the policy value was always overridden
in Qt6Config.cmake.

To actually make the diagnostic work, there is now new code to save
the policy value in the current directory scope, before Qt resets
it.
This only works if a project uses the find_package(Qt6 COMPONENTS Foo)
signature. It won't work with a find_package(Qt6Core)-like signature.

The policy value is not modified for platforms other than Apple ones
for now.

Amends 9702c3c78b2c16db6a9d0515d7d7698d9b064cd8

Pick-to: 6.8 6.5
Fixes: QTBUG-135978
Change-Id: I4d6e6c2a01e7092b417fc669d2aea40cf2dca578
Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
(cherry picked from commit c20d7bcb86361d0c9f8af3807dcad9db1a3a5ca0)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
This commit is contained in:
Alexandru Croitor 2025-05-05 17:18:49 +02:00
parent 963a357e88
commit 4f0591c807
17 changed files with 200 additions and 15 deletions

View File

@ -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"

View File

@ -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.

View File

@ -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")

View File

@ -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()

View File

@ -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()

View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
project(${RunCMake_TEST} LANGUAGES CXX OBJCXX)
include(${RunCMake_TEST}.cmake)

View File

@ -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)

View File

@ -0,0 +1,22 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <Foundation/Foundation.h>
// 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; };

View File

@ -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; };

View File

@ -0,0 +1,13 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGui>
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();
}

View File

@ -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)

View File

@ -0,0 +1 @@
include(project-common.cmake)

View File

@ -0,0 +1 @@
include(project-common.cmake)

View File

@ -0,0 +1 @@
CMP0156 is set to 'OLD'

View File

@ -0,0 +1 @@
include(project-common.cmake)

View File

@ -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()