CMake: Handle automatic rpath embedding correctly

Instead of using CMAKE_INSTALL_RPATH to embed an absolute path
to prefix/libdir into all targets, use the more sophisticated aproach
that qmake does.

For certain targets (modules, plugins, tools) use relative rpaths.
Otherwise embed absolute paths (examples, regular binaries).
Installed tests currently have no rpaths.

On certain platforms rpaths are not used (Windows, Android,
iOS / uikit).

Frameworks, app bundles and shallow bundles should also be handled
correctly.

Additional rpaths can be provided via QT_EXTRA_RPATHS variable
(similar to the -R option that configure takes).

Automatic embedding can be disabled either via QT_FEATURE_rpath=OFF
or QT_DISABLE_RPATH=ON.

Note that installed examples are not relocatable at the moment (due
to always having an absolute path rpath), so this is a missing feature
compared to qmake. This is due to missing information on where
examples will be installed, so a relative rpath can not be computed.

By default a Qt installation is relocatable, so there is no need to
pass -DQT_EXTRA_RPATHS=. like Coin used to do with qmake e.g. -R .

Relative rpaths will have the appropriate 'relative base' prefixed
to them (e.g $ORIGIN on linux and @loader_path on darwin platforms).
There is currently no support for other platforms that might have a
different 'relative base' than the ones mentioned above.

Any extra rpaths are saved to BuildInternalsExtra which are re-used
when building other repositories.

configurejson2cmake modified to include correct conditions for the
rpath feature.

It's very likely that we will need a new qt_add_internal_app()
function for gui apps that are to be installed to prefix/bin.
For example for Assistant from qttools. Currently such apps
use qt_add_executable().
The distinction is necessary to make sure that relative rpaths are
embedded into apps, but not executables (which tests are part of).

Amends e835a6853b

Task-number: QTBUG-83497
Change-Id: I3510f63c0a59489741116cc8ec3ef6a0a7704f25
Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
This commit is contained in:
Alexandru Croitor 2020-04-15 18:48:26 +02:00
parent e6d880e511
commit 67ee92f4d8
6 changed files with 189 additions and 10 deletions

View File

@ -153,6 +153,11 @@ function(qt_auto_detect_ios)
message(FATAL_ERROR
"Building Qt for ${CMAKE_SYSTEM_NAME} as shared libraries is not supported.")
endif()
# Disable qt rpaths for iOS, just like mkspecs/common/uikit.conf does, due to those
# bundles not being able to use paths outside the app bundle. Not sure this is strictly
# needed though.
set(QT_DISABLE_RPATH "OFF" CACHE BOOL "Disable automatic Qt rpath handling." FORCE)
endif()
endfunction()

View File

@ -106,13 +106,12 @@ if("${isSystemDir}" STREQUAL "-1")
set(_default_install_rpath "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBDIR}")
endif("${isSystemDir}" STREQUAL "-1")
# Default rpath settings: Use rpath for build tree as well as a full path for the installed binaries.
# For origin builds, one needs to override CMAKE_INSTALL_RPATH for example with $ORIGIN/../lib
# Example: -DCMAKE_INSTALL_RPATH=\$ORIGIN/../lib (backslash to escape the $ in the shell)
# Implementation note: the cache var must be STRING and not PATH or FILEPATH, otherwise CMake will
# transform the value into an absolute path, getting rid of '$ORIGIN'.
set(CMAKE_INSTALL_RPATH "${_default_install_rpath}" CACHE STRING "RPATH for installed binaries")
message(STATUS "Install RPATH set to: ${CMAKE_INSTALL_RPATH}")
# The default rpath settings for installed targets is empty.
# The rpaths will instead be computed for each target separately using qt_apply_rpaths().
# Additional rpaths can be passed via QT_EXTRA_RPATHS.
# By default this will include $ORIGIN / @loader_path, so the installation is relocatable.
# Bottom line: No need to pass anything to CMAKE_INSTALL_RPATH.
set(CMAKE_INSTALL_RPATH "" CACHE STRING "RPATH for installed binaries")
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
@ -1976,6 +1975,8 @@ set(QT_CMAKE_EXPORT_NAMESPACE ${QT_CMAKE_EXPORT_NAMESPACE})")
PRIVATE_HEADER DESTINATION ${INSTALL_INCLUDEDIR}/${module}/${PROJECT_VERSION}/${module}/private
)
qt_apply_rpaths(TARGET "${target}" INSTALL_PATH "${INSTALL_LIBDIR}" RELATIVE_RPATH)
if (ANDROID AND NOT arg_HEADER_MODULE)
# Record install library location so it can be accessed by
# qt_android_dependencies without having to specify it again.
@ -2478,6 +2479,7 @@ function(qt_add_plugin target)
NAMESPACE ${QT_CMAKE_EXPORT_NAMESPACE}::
DESTINATION "${config_install_dir}"
)
qt_apply_rpaths(TARGET "${target}" INSTALL_PATH "${install_directory}" RELATIVE_RPATH)
endif()
# Store the plug-in type in the target property
@ -3378,6 +3380,8 @@ function(qt_add_tool name)
qt_install(TARGETS "${name}"
EXPORT "${INSTALL_CMAKE_NAMESPACE}${arg_TOOLS_TARGET}ToolsTargets"
DESTINATION ${INSTALL_TARGETS_DEFAULT_ARGS})
qt_apply_rpaths(TARGET "${name}" INSTALL_PATH "${INSTALL_BINDIR}" RELATIVE_RPATH)
endif()
if(QT_FEATURE_separate_debug_info AND (UNIX OR MINGW))
@ -4150,6 +4154,140 @@ function(qt_exclude_tool_directories_from_default_target)
endif()
endfunction()
function(qt_compute_relative_rpath_base rpath install_location out_var)
set(install_lib_dir_absolute "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBDIR}")
get_filename_component(rpath_absolute "${rpath}"
ABSOLUTE BASE_DIR "${install_lib_dir_absolute}")
if(NOT IS_ABSOLUTE)
set(install_location_absolute "${CMAKE_INSTALL_PREFIX}/${install_location}")
endif()
# Compute relative rpath from where the target will be installed, to the place where libraries
# will be placed (INSTALL_LIBDIR).
file(RELATIVE_PATH rpath_relative "${install_location_absolute}" "${rpath_absolute}")
if("${rpath_relative}" STREQUAL "")
# file(RELATIVE_PATH) returns an empty string if the given absolute paths are equal
set(rpath_relative ".")
endif()
# Prepend $ORIGIN / @loader_path style tokens (qmake's QMAKE_REL_RPATH_BASE), to make the
# relative rpaths work. qmake does this automatically when generating a project, so it wasn't
# needed in the .prf files, but for CMake we need to prepend them ourselves.
if(APPLE)
set(rpath_rel_base "@loader_path")
elseif(LINUX)
set(rpath_rel_base "$ORIGIN")
else()
message(WARNING "No known RPATH_REL_BASE for target platform.")
set(rpath_rel_base "NO_KNOWN_RPATH_REL_BASE")
endif()
if(rpath_relative STREQUAL ".")
set(rpath_relative "${rpath_rel_base}")
else()
set(rpath_relative "${rpath_rel_base}/${rpath_relative}")
endif()
set("${out_var}" "${rpath_relative}" PARENT_SCOPE)
endfunction()
# Applies necessary rpaths to a target upon target installation.
# No-op when targeting Windows, Android, or non-prefix builds.
#
# If no RELATIVE_RPATH option is given, embeds an absolute path rpath to ${INSTALL_LIBDIR}.
# If RELATIVE_RPATH is given, the INSTALL_PATH value is to compute the relative path from
# ${INSTALL_LIBDIR} to wherever the target will be installed (the value of INSTALL_PATH).
# It's the equivalent of qmake's relative_qt_rpath.
# INSTALL_PATH is used to implement the equivalent of qmake's $$qtRelativeRPathBase().
#
# A cache variable QT_DISABLE_RPATH can be set to disable embedding any rpaths when installing.
function(qt_apply_rpaths)
# No rpath support for win32 and android. Also no need to apply rpaths when doing a non-prefix
# build.
if(NOT QT_WILL_INSTALL OR WIN32 OR ANDROID)
return()
endif()
# Rpaths xplicitly disabled (like for uikit), equivalent to qmake's no_qt_rpath.
if(QT_DISABLE_RPATH)
return()
endif()
qt_parse_all_arguments(arg "qt_apply_rpaths" "RELATIVE_RPATH" "TARGET;INSTALL_PATH" "" ${ARGN})
if(NOT arg_TARGET)
message(FATAL_ERRO "No target given to qt_apply_rpaths.")
else()
set(target "${arg_TARGET}")
endif()
# If a target is not built (which can happen for tools when crosscompiling, we shouldn't try
# to apply properties.
if(NOT TARGET "${target}")
return()
endif()
# Protect against interface libraries.
get_target_property(target_type "${target}" TYPE)
if (target_type STREQUAL "INTERFACE_LIBRARY")
return()
endif()
if(NOT arg_INSTALL_PATH)
message(FATAL_ERROR "No INSTALL_PATH given to qt_apply_rpaths.")
endif()
set(rpaths "")
# Modify the install path to contain the nested structure of a framework.
get_target_property(is_framework "${target}" FRAMEWORK)
if(is_framework)
if(UIKIT)
# Shallow framework
string(APPEND arg_INSTALL_PATH "/Qt${target}.framework")
else()
# Full framework
string(APPEND arg_INSTALL_PATH "/Qt${target}.framework/Versions/Current")
endif()
endif()
# Same but for an app bundle.
get_target_property(is_bundle "${target}" MACOSX_BUNDLE)
if(is_bundle AND NOT is_framework)
if(UIKIT)
# Shallow bundle
string(APPEND arg_INSTALL_PATH "/${target}.app")
else()
# Full bundle
string(APPEND arg_INSTALL_PATH "/${target}.app/Contents/MacOS")
endif()
endif()
# Somewhat similar to mkspecs/features/qt.prf
if(arg_RELATIVE_RPATH)
qt_compute_relative_rpath_base(
"${_default_install_rpath}" "${arg_INSTALL_PATH}" relative_rpath)
list(APPEND rpaths "${relative_rpath}")
else()
list(APPEND rpaths "${_default_install_rpath}")
endif()
# Somewhat similar to mkspecs/features/qt_build_extra.prf.
foreach(rpath ${QT_EXTRA_RPATHS})
if(IS_ABSOLUTE)
list(APPEND rpaths "${rpath}")
else()
qt_compute_relative_rpath_base("${rpath}" "${arg_INSTALL_PATH}" relative_rpath)
list(APPEND rpaths "${relative_rpath}")
endif()
endforeach()
if(rpaths)
list(REMOVE_DUPLICATES rpaths)
set_property(TARGET "${target}" APPEND PROPERTY INSTALL_RPATH ${rpaths})
endif()
endfunction()
# Compatibility macros that should be removed once all their usages are removed.
function(extend_target)
qt_extend_target(${ARGV})

View File

@ -259,6 +259,13 @@ macro(qt_examples_build_begin)
set(QT_NO_CREATE_TARGETS TRUE)
set(BACKUP_CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ${CMAKE_FIND_ROOT_PATH_MODE_PACKAGE})
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE "BOTH")
# Because CMAKE_INSTALL_RPATH is empty by default in the repo project, examples need to have
# it set here, so they can run when installed.
# This means that installed examples are not relocatable at the moment. We would need to
# annotate where each example is installed to, to be able to derive a relative rpath, and it
# seems there's no way to query such information from CMake itself.
set(CMAKE_INSTALL_RPATH "${_default_install_rpath}")
endmacro()
macro(qt_examples_build_end)

View File

@ -366,6 +366,18 @@ function(qt_generate_build_internals_extra_cmake_code)
"set(BUILD_WITH_PCH \"${BUILD_WITH_PCH}\" CACHE STRING \"\")\n")
endif()
# Rpath related things that need to be re-used when building other repos.
string(APPEND QT_EXTRA_BUILD_INTERNALS_VARS
"set(CMAKE_INSTALL_RPATH \"${CMAKE_INSTALL_RPATH}\" CACHE STRING \"\")\n")
if(DEFINED QT_DISABLE_RPATH)
string(APPEND QT_EXTRA_BUILD_INTERNALS_VARS
"set(QT_DISABLE_RPATH \"${QT_DISABLE_RPATH}\" CACHE STRING \"\")\n")
endif()
if(DEFINED QT_EXTRA_RPATHS)
string(APPEND QT_EXTRA_BUILD_INTERNALS_VARS
"set(QT_EXTRA_RPATHS \"${QT_EXTRA_RPATHS}\" CACHE STRING \"\")\n")
endif()
qt_generate_install_prefixes(install_prefix_content)
string(APPEND QT_EXTRA_BUILD_INTERNALS_VARS "${install_prefix_content}")

View File

@ -397,7 +397,7 @@ qt_feature_config("simulator_and_device" QMAKE_PUBLIC_QT_CONFIG)
qt_feature("rpath" PUBLIC
LABEL "Build with RPATH"
AUTODETECT 1
CONDITION BUILD_SHARED_LIBS AND UNIX AND NOT WIN32 AND NOT ANDROID AND NOT APPLE
CONDITION BUILD_SHARED_LIBS AND UNIX AND NOT WIN32 AND NOT ANDROID
)
qt_feature_config("rpath" QMAKE_PUBLIC_QT_CONFIG)
qt_feature("force_asserts" PUBLIC
@ -919,6 +919,11 @@ qt_configure_add_report_entry(
MESSAGE "debug-only framework builds are not supported. Configure with -no-framework if you want a pure debug build."
CONDITION QT_FEATURE_framework AND QT_FEATURE_debug AND NOT QT_FEATURE_debug_and_release
)
qt_configure_add_report_entry(
TYPE ERROR
MESSAGE "Static builds don't support RPATH"
CONDITION ( QT_FEATURE_rpath OR QT_EXTRA_RPATHS ) AND NOT QT_FEATURE_shared
)
qt_configure_add_report_entry(
TYPE ERROR
MESSAGE "Command line option -coverage is only supported with clang compilers."

View File

@ -876,10 +876,9 @@ def get_feature_mapping():
"qpa_default_platform": None, # Not a bool!
"release": None,
"release_tools": None,
"rpath_dir": None, # merely used to fill the qmake variable EXTRA_RPATHS
"rpath": {
"autoDetect": "1",
"condition": "BUILD_SHARED_LIBS AND UNIX AND NOT WIN32 AND NOT ANDROID AND NOT APPLE",
"condition": "BUILD_SHARED_LIBS AND UNIX AND NOT WIN32 AND NOT ANDROID",
},
"sanitize_address": None, # sanitizer
"sanitize_memory": None,
@ -1222,6 +1221,12 @@ def processSummaryHelper(ctx, entries, cm_fh):
print(f" XXXX UNHANDLED SUMMARY TYPE {entry}.")
report_condition_mapping = {
"(features.rpath || features.rpath_dir) && !features.shared": "(features.rpath || QT_EXTRA_RPATHS) && !features.shared",
"(features.rpath || features.rpath_dir) && var.QMAKE_LFLAGS_RPATH == ''": None
}
def processReportHelper(ctx, entries, cm_fh):
feature_mapping = get_feature_mapping()
@ -1265,6 +1270,13 @@ def processReportHelper(ctx, entries, cm_fh):
if unhandled_condition:
print(f" XXXX UNHANDLED CONDITION in REPORT TYPE {entry}.")
continue
if isinstance(condition, str) and condition in report_condition_mapping:
new_condition = report_condition_mapping[condition]
if new_condition is None:
continue
else:
condition = new_condition
condition = map_condition(condition)
entry_args.append(lineify("CONDITION", condition, quote=False))
entry_args_string = "".join(entry_args)