CMake and a relative path to an installed linker script
2019-10-15
Scenario: A static library lib
is to be installed. To link against this static library requires special link flags and a linker script script.ld
.
For “GCC compatible” linkers the linker script is specified via the -T
flag, followed by the path to the script. During build of the static library itself the linker script is within the project source directory; referencing that is no different than any other file. Assuming the linker script is put under ${CMAKE_INSTALL_PREFIX}/share/script.ld
when installed the straightforward solution looks like this:
# -T path/to/script.ld for build and install case
target_link_options(lib INTERFACE
$<BUILD_INTERFACE:script.ld> $<INSTALL_INTERFACE:share/script.ld>)
-T
# actually install script.ld file
install(FILES script.ld DESTINATION share/)
# install liblib.a library, prepare export
install(TARGETS lib
EXPORT lib-target
ARCHIVE DESTINATION lib/)
# install export (cmake files to be used by consumers of the lib)
install(EXPORT lib-target
FILE libTargets.cmake
NAMESPACE lib::
DESTINATION cmake/)
This works as expected; assuming an empty ${CMAKE_INSTALL_PREFIX}
it produces a /lib/liblib.a
, a /share/script.ld
and a /cmake/libTargets.cmake
(and probably also a /cmake/libTargets-noconfig.cmake
) when installing via make install
.
If /cmake/
is part of the CMake module search path or if -Dlib_DIR=/cmake
is specified then the library can be used in another CMake build using find_package(lib)
and target_link_libraries(target lib::lib)
. The link flags will contain -T /share/script.ld
and of course this path is valid, so everything is working fine.
This does not work together with a staging directory (DESTDIR
), though!
E.g. for the library make install DESTDIR=/stage
is called (perhaps because the library shall not actually be installed). Now using the “almost installed” library in /stage
from another CMake build (e.g. -Dlib_DIR=/stage/cmake
) works fine (the liblib.a
is found as would any installed public headers) except for the linker script. Its path is hard wired to /share/script.ld
after installing (even if only “almost”). The script is in /stage/share/script.ld
, though.
To fix this the best is to look at what happens with the path to liblib.a
, because somehow CMake can find it even in the “almost installed” /stage
directory:
/stage/cmake/libTargets.cmake
includes (relative to itself) libTargets-noconfig.cmake
. That later file contains code similar to:
set_target_properties(lib::lib PROPERTIES
IMPORTED_LOCATION_NOCONFIG "${_IMPORT_PREFIX}/lib/liblib.a")
What’s ${_IMPORT_PREFIX}
? It’s a variable computed in /stage/cmake/libTargets.cmake
:
# Compute the installation prefix relative to this file.
get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)
# Use original install prefix when loaded through a
# cross-prefix symbolic link such as /lib -> /usr/lib.
get_filename_component(_realCurr "${_IMPORT_PREFIX}" REALPATH)
get_filename_component(_realOrig "/cmake" REALPATH)
if(_realCurr STREQUAL _realOrig)
set(_IMPORT_PREFIX "/cmake")
endif()
unset(_realOrig)
unset(_realCurr)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
#
# (HERE, read about it below)
#
if(_IMPORT_PREFIX STREQUAL "/")
set(_IMPORT_PREFIX "")
endif()
This basically walks up from the location of libTargets.cmake
to where ${CMAKE_INSTALL_PREFIX}
would be. In this case, since libTargets.cmake
is to be installed in ${CMAKE_INSTALL_PREFIX}/cmake/
it goes up just one level (at the HERE
comment), but if it had been put e.g. to ${CMAKE_INSTALL_PREFIX}/lib/share/cmake/
then there would be 3 get_filename_component
calls instead of just one.
That way, at “use time” (e.g. during the find_package(lib)
call) CMake uses /stage/cmake/libTargets.cmake
to compute an ${_IMPORT_PREFIX}
of /stage
which allows it to use ${_IMPORT_PREFIX}/lib/liblib.a
to refer to /stage/lib/liblib.a
- the correct path to the static library. The important point is that this happens not as part of the configure / build / install process of the library, but rather during the configuration of another CMake build (which uses find_package(lib)
).
Fixing the issue with the path to the linker script can be done in (at least) two ways now:
CMake already computes the very helpful _IMPORT_PREFIX
variable. In order to get “access” to it code needs to be either in libTargets.cmake
or in any file included from that file. Modifying the generated file libTargets.cmake
is not a good idea, but thankfully libTargets.cmake
contains code to include files libTargets-*.cmake
(like libTargets-noconfig.cmake
):
# Load information for each installed configuration.
get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
file(GLOB CONFIG_FILES "${_DIR}/libTargets-*.cmake")
foreach(f ${CONFIG_FILES})
include(${f})
endforeach()
A custom libTargets-script.cmake
file installed to cmake/
has therefore access to _IMPORT_PREFIX
and can use it to set link options appropriately:
# contents of libTargets-script.cmake
# note use of namespaced target name!
target_link_options(lib::lib
INTERFACE
${_IMPORT_PREFIX}/share/script.ld) -T
Whether this is “supported” (it works, but will it in the future?) by CMake? I don’t know.
A thus possibly more robust (but also more verbose) approach is to recreate what CMake does with the _IMPORT_PREFIX
variable in a (self-written) libConfig.cmake
, to be installed to cmake/
:
# contents of libConfig.cmake.in
get_filename_component(MYDIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
if (NOT TARGET lib::lib)
include("${MYDIR}/multipleTargets.cmake")
endif ()
# Compute the installation prefix relative to this file, like
# CMake does in libTargets.cmake .. this needs to be kept in sync and
# needs to be adjusted e.g. if the (installed) path to this file changes
get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_realCurr "${_IMPORT_PREFIX}" REALPATH)
get_filename_component(_realOrig "@CMAKE_INSTALL_PREFIX@/cmake" REALPATH)
if(_realCurr STREQUAL _realOrig)
set(_IMPORT_PREFIX "@CMAKE_INSTALL_PREFIX@/cmake")
endif()
unset(_realOrig)
unset(_realCurr)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
if(_IMPORT_PREFIX STREQUAL "/")
set(_IMPORT_PREFIX "")
endif()
# note use of namespaced target name!
target_link_options(lib::lib
INTERFACE
${_IMPORT_PREFIX}/share/script.ld) -T
The above file is libConfig.cmake.in
, which needs to undergo substitution of @CMAKE_INSTALL_PREFIX@
via configure_file()
with @ONLY
before installing.
With either of these approaches it is possible to install a linker script and use it from other builds, even if the installation was only to a staging directory. And of course the staging directory can be moved around freely … e.g mv /stage /somewhere/else
doesn’t break the build.
Finally, this works for every path, e.g. also paths to data files.