qtbase/cmake/QtBaseHelpers.cmake
Alexandru Croitor 4f0591c807 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>
2025-05-16 23:38:37 +02:00

232 lines
9.0 KiB
CMake

# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#
# Note that this file is not installed.
# Bail out if any part of the build directory's path is symlinked.
function(qt_internal_check_if_path_has_symlinks path)
get_filename_component(dir "${path}" ABSOLUTE)
set(is_symlink FALSE)
if(CMAKE_HOST_WIN32)
# CMake marks Windows mount points as symbolic links, so use simplified REALPATH check
# on Windows platforms instead of IS_SYMLINK.
get_filename_component(dir_realpath "${dir}" REALPATH)
if(NOT dir STREQUAL dir_realpath)
set(is_symlink TRUE)
endif()
else()
while(TRUE)
if(IS_SYMLINK "${dir}")
set(is_symlink TRUE)
break()
endif()
set(prev_dir "${dir}")
get_filename_component(dir "${dir}" DIRECTORY)
if("${dir}" STREQUAL "${prev_dir}")
return()
endif()
endwhile()
endif()
if(is_symlink)
set(possible_solutions_for_resolving_symlink [[
- Map directories using a transparent mechanism such as mount --bind
- Pass the real path of the build directory to CMake, e.g. using
cd $(realpath <path>) before invoking cmake <source_dir>.
]])
if(QT_ALLOW_SYMLINK_IN_PATHS)
# In some cases, e.g., Homebrew, it is beneficial to skip this check.
# Before this, Homebrew had to patch this out to be able to get their build.
message(WARNING
"The path \"${path}\" contains symlinks. "
"This is not recommended, and it may lead to unexpected issues. If you do "
"not have a good reason for enabling 'QT_ALLOW_SYMLINK_IN_PATHS', disable "
"it, and follow one of the following solutions: \n"
"${possible_solutions_for_resolving_symlink} ")
else()
message(FATAL_ERROR
"The path \"${path}\" contains symlinks. "
"This is not supported. Possible solutions: \n"
"${possible_solutions_for_resolving_symlink} ")
endif()
endif()
endfunction()
# There are three necessary copies of this macro in
# qtbase/cmake/QtBaseHelpers.cmake
# qtbase/cmake/QtBaseTopLevelHelpers.cmake
# qtbase/cmake/QtBuildRepoHelpers.cmake
macro(qt_internal_qtbase_setup_standalone_parts)
# A generic marker for any kind of standalone builds, either tests or examples.
if(NOT DEFINED QT_INTERNAL_BUILD_STANDALONE_PARTS
AND (QT_BUILD_STANDALONE_TESTS OR QT_BUILD_STANDALONE_EXAMPLES))
set(QT_INTERNAL_BUILD_STANDALONE_PARTS TRUE CACHE INTERNAL
"Whether standalone tests or examples are being built")
endif()
endmacro()
macro(qt_internal_qtbase_run_autodetect)
qt_internal_qtbase_setup_standalone_parts()
# Run auto detection routines, but not when doing standalone tests or standalone examples.
# In that case, the detection
# results are taken from either QtBuildInternals or the qt.toolchain.cmake file. Also, inhibit
# auto-detection in a top-level build, because the top-level project file already includes it.
if(NOT QT_INTERNAL_BUILD_STANDALONE_PARTS AND NOT QT_SUPERBUILD)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/QtAutoDetect.cmake)
endif()
endmacro()
macro(qt_internal_qtbase_pre_project_setup)
if(NOT QT_INTERNAL_BUILD_STANDALONE_PARTS)
# Should this Qt be static or dynamically linked?
option(BUILD_SHARED_LIBS "Build Qt statically or dynamically" ON)
set(QT_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
# This variable is also set in Qt6CoreConfigExtras.cmake, but it's not loaded when building
# qtbase. Set it here so qt_add_plugin can compute the proper plugin flavor.
set(QT6_IS_SHARED_LIBS_BUILD ${BUILD_SHARED_LIBS})
# BUILD_SHARED_LIBS influences the minimum required CMake version. The value is set either
# by:
# a cache variable provided on the configure command line
# or set by QtAutoDetect.cmake depending on the platform
# or specified via a toolchain file that is loaded by the project() call
# or set by the option() call above
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/QtCMakeVersionHelpers.cmake")
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"
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/3rdparty/extra-cmake-modules/find-modules"
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/3rdparty/kwin"
)
if(MACOS)
# Add module directory to pick up custom Info.plist template
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos")
elseif(IOS)
# Add module directory to pick up custom Info.plist template
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/ios")
endif()
## Find the build internals package.
set(QT_BUILD_INTERNALS_SKIP_CMAKE_MODULE_PATH_ADDITION TRUE)
list(PREPEND CMAKE_PREFIX_PATH
"${CMAKE_CURRENT_SOURCE_DIR}/cmake"
)
find_package(QtBuildInternals CMAKE_FIND_ROOT_PATH_BOTH)
unset(QT_BUILD_INTERNALS_SKIP_CMAKE_MODULE_PATH_ADDITION)
else()
# When building standalone parts, an istalled BuildInternals package already exists.
find_package(Qt6 REQUIRED COMPONENTS BuildInternals CMAKE_FIND_ROOT_PATH_BOTH)
endif()
endmacro()
macro(qt_internal_qtbase_install_mkspecs)
# As long as we use the mkspecs (for qplatformdefs.h), we need to always
# install it, especially when cross-compiling.
set(mkspecs_install_dir "${INSTALL_MKSPECSDIR}")
qt_path_join(mkspecs_install_dir ${QT_INSTALL_DIR} ${mkspecs_install_dir})
file(GLOB mkspecs_subdirs
LIST_DIRECTORIES TRUE
"${PROJECT_SOURCE_DIR}/mkspecs/*")
foreach(entry IN LISTS mkspecs_subdirs)
if (IS_DIRECTORY ${entry})
qt_copy_or_install(DIRECTORY "${entry}"
DESTINATION ${mkspecs_install_dir}
USE_SOURCE_PERMISSIONS)
else()
qt_copy_or_install(FILES "${entry}"
DESTINATION ${mkspecs_install_dir})
endif()
endforeach()
endmacro()
macro(qt_internal_qtbase_build_repo)
qt_internal_qtbase_pre_project_setup()
qt_internal_project_setup()
qt_build_repo_begin()
if(NOT QT_INTERNAL_BUILD_STANDALONE_PARTS)
## Should this Qt be built with Werror?
option(WARNINGS_ARE_ERRORS "Build Qt with warnings as errors" ${FEATURE_developer_build})
## Should this Qt create versioned hard link for some tools?
option(QT_CREATE_VERSIONED_HARD_LINK "Enable the use of versioned hard link" ON)
## QtBase specific configure tests:
include(QtBaseConfigureTests)
## Build System tests:
include(QtBaseCMakeTesting)
## Include CoreMacros() for qt6_generate_meta_types()
## Also needed for the QtBaseGlobalTargets creation.
set(QT_DEFAULT_MAJOR_VERSION 6)
include(src/corelib/Qt6CoreMacros.cmake)
# Needed when building qtbase for android.
if(ANDROID)
include(src/corelib/Qt6AndroidMacros.cmake)
endif()
# Needed when building for WebAssembly.
if(WASM)
include(cmake/QtWasmHelpers.cmake)
include(src/corelib/Qt6WasmMacros.cmake)
endif()
## Targets for global features, etc.:
include(QtBaseGlobalTargets)
## Set language standards after QtBaseGlobalTargets, because that's when the relevant
## feature variables are available.
qt_set_language_standards()
if(ANDROID)
_qt_internal_create_global_android_targets()
endif()
if(WASM)
qt_internal_setup_wasm_target_properties(Platform)
endif()
# Set up optimization flags like in qmake.
# This function must be called after the global QT_FEATURE_xxx variables have been set up,
# aka after QtBaseGlobalTargets is processed.
# It also has to be called /before/ adding add_subdirectory(src), so that per-directory
# modifications can still be applied if necessary (like in done in Core and Gui).
qt_internal_set_up_config_optimizations_like_in_qmake()
## Setup documentation
add_subdirectory(doc)
## Visit all the directories:
add_subdirectory(src)
endif()
if(NOT QT_INTERNAL_BUILD_STANDALONE_PARTS)
if(QT_FEATURE_settings)
add_subdirectory(qmake)
endif()
qt_internal_qtbase_install_mkspecs()
endif()
qt_build_repo_post_process()
qt_build_repo_impl_tests()
qt_build_repo_end()
qt_build_repo_impl_examples()
endmacro()