correctly set the location of imported cmake targets for an installed package
The main issue is that the two files foobarLibTargets.cmake
and foobarTargets.cmake
were both installed and the wrong one was picked up.
You will find below an improved project along with remarks to better organize the build system.
ChangeLog summarizing edits
- 2019-05-25
- Create GitHub project to streamline reuse and adaptation. See https://github.com/jcfr/stackoverflow-56135785-answer
- Rename project and source directory from
foobar
toFooBarLib
, update Suggestions section accordingly - Improve
build.sh
- Updated suggestions (
CPACK_PACKAGING_INSTALL_PREFIX
should be absolute) - RPM:
- Add support for building RPM package using
make package
- Update
build.sh
to display content of RPM package
- Add support for building RPM package using
Remarks
- Two config files should be generated:
- one for the build tree: this allow user of your project to directly build against your project and import targets
- one for the install tree (which also end up being packaged)
- Do not force the value of
CMAKE_INSTALL_PREFIX
CPACK_PACKAGING_INSTALL_PREFIX
should NOT be set to an absolute directoryFor sake of consistency, usefoobarTargets
instead offoobarLibTargets
<projecname_uc>
placeholder used below correspond to the name of the project upper-cased (ABC
instead ofabc
)- To allow configuring your project when vendorized along other one, prefer variable with
<projecname_uc>_
. This means<projecname_uc>_INSTALL_LIBRARY_DIR
is better thanLIBRARY_INSTALL_DIR
. - To allow user of the project to configure
*_INSTALL_DIR
variables, wrap them aroundif(DEFINED ...)
- Consistently use variables (e.g
LIBRARY_INSTALL_DIR
should always be used instead oflib
) - Prefer naming variable
<projecname_uc>_INSTALL_*_DIR
instead of<projecname_uc>_*_INSTALL_DIR
, it make it easier to know the purpose of the variable when reading the code. - Since version is already associated with the project, there is no need to set
VERSION
variable. Instead, you can usePROJECT_VERSION
orFOOBAR_VERSION
- If starting a new project, prefer the most recent CMake version. CMake 3.13 instead of CMake 3.7
- Introduced variable
<projecname_uc>_INSTALL_CONFIG_DIR
<project_name>Targets.cmake
should not be installed usinginstall(FILES ...)
, it is already associated with an install rule- conditionally set
CMAKE_INSTALL_RPATH
, it is valid only on Linux <project_name>Config.cmake.in
:- there is no need to set
FOOBAR_LIBRARY
, this information is already associated with the exportedfoobar
target - FOOBAR_LIBRARY_DIR is also not needed, this information is already associated with the exported
foobar
target - instead of setting FOOBAR_INCLUDE_DIR, the command
target_include_directories
should be used - remove setting of
FOOBAR_VERSION
, the generate version file already takes care of setting the version.
- there is no need to set
- always specify ARCHIVE, LIBRARY and RUNTIME when declaring install rules for target. It avoid issue when switching library type. One less thing to think about.
- always specify component with your install rule. It allows user of your project to selectively install part of it only development component or only runtime one, ...
- initializing
CMAKE_BUILD_TYPE
is also important, it ensures the generated Targets file are associated with a configuration (instead of having the suffix-noconfig.cmake
)
Suggested changes
Generally speaking, I recommend to have a source tree, a build tree and install tree. The files posted below assumed the following layout:
./build.sh
./FooBarLib/FooBarLibConfig.cmake.in
./FooBarLib/CMakeLists.txt
./FooBarLib/foobar.cpp
./FooBarLib/foobar.h
./FooBarLib-build
./FooBarLib-install
./useFoo/CMakeLists.txt
./useFoo-build
- build.sh
#!/bin/bash
set -xeu
set -o pipefail
script_dir=$(cd $(dirname $0) || exit 1; pwd)
project_name=FooBarLib
archive_name=${project_name}
# cleanup ${project_name}-build
cd $script_dir
rm -rf ${project_name}-build
mkdir ${project_name}-build
cd ${project_name}-build
# configure, build and package ${project_name}
cmake ../${project_name}
make
make package # equivalent to running "cpack -G TGZ" and "cmake -G RPM"
# extract ${project_name} archive
cd $script_dir
rm -rf ${project_name}-install
mkdir ${project_name}-install
cd ${project_name}-install
tar -xvzf ../${project_name}-build/${archive_name}-1.2.3.tar.gz
# cleanup useFoo-build
cd $script_dir
rm -rf useFoo-build
mkdir useFoo-build
cd useFoo-build
cpack_install_prefix=/opt
# configure useFoo
cmake -D${project_name}_DIR=$script_dir/${project_name}-install${cpack_install_prefix}/lib/cmake/${project_name}/ ../useFoo
cat foobar-gen
# display content of RPM. If command "rpmbuild" is available, RPM package is expected.
if command -v rpmbuild &> /dev/null; then
rpm -qlp $script_dir/${project_name}-build/${archive_name}-1.2.3.rpm
fi
- FooBarLib/CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
project(FooBarLib VERSION 1.2.3)
if(UNIX AND NOT APPLE)
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib:$ORIGIN/")
endif()
#------------------------------------------------------------------------------
# Set a default build type if none was specified
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message(STATUS "Setting build type to 'Release' as none was specified.")
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE)
mark_as_advanced(CMAKE_BUILD_TYPE)
# Set the possible values of build type for cmake-gui
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()
#------------------------------------------------------------------------------
# This variable controls the prefix used to generate the following files:
# <export_config_name>ConfigVersion.cmake
# <export_config_name>Config.cmake
# <export_config_name>Targets.cmake
# and it also used to initialize FOOBARLIB_INSTALL_CONFIG_DIR value.
set(export_config_name ${PROJECT_NAME})
#------------------------------------------------------------------------------
if(NOT DEFINED FOOBARLIB_INSTALL_INCLUDE_DIR)
set(FOOBARLIB_INSTALL_INCLUDE_DIR include)
endif()
if(NOT DEFINED FOOBARLIB_INSTALL_BIN_DIR)
set(FOOBARLIB_INSTALL_BIN_DIR bin)
endif()
if(NOT DEFINED FOOBARLIB_INSTALL_LIBRARY_DIR)
set(FOOBARLIB_INSTALL_LIBRARY_DIR lib)
endif()
if(NOT DEFINED FOOBARLIB_INSTALL_CONFIG_DIR)
set(FOOBARLIB_INSTALL_CONFIG_DIR ${FOOBARLIB_INSTALL_LIBRARY_DIR}/cmake/${export_config_name})
endif()
#------------------------------------------------------------------------------
set(headers
foobar.h
)
# Install rule for headers
install(
FILES ${headers}
DESTINATION ${FOOBARLIB_INSTALL_INCLUDE_DIR}
COMPONENT Development
)
#------------------------------------------------------------------------------
add_library(foobar SHARED
foobar.cpp
)
target_include_directories(foobar
PUBLIC
$<BUILD_INTERFACE:${FooBarLib_SOURCE_DIR}>
$<INSTALL_INTERFACE:${FOOBARLIB_INSTALL_INCLUDE_DIR}>
)
install(
TARGETS foobar
EXPORT ${export_config_name}Targets
ARCHIVE DESTINATION ${FOOBARLIB_INSTALL_LIBRARY_DIR} COMPONENT Development
LIBRARY DESTINATION ${FOOBARLIB_INSTALL_LIBRARY_DIR} COMPONENT RuntimeLibraries
RUNTIME DESTINATION ${FOOBARLIB_INSTALL_BIN_DIR} COMPONENT RuntimeLibraries
)
#------------------------------------------------------------------------------
# Configure <export_config_name>ConfigVersion.cmake common to build and install tree
include(CMakePackageConfigHelpers)
set(config_version_file ${PROJECT_BINARY_DIR}/${export_config_name}ConfigVersion.cmake)
write_basic_package_version_file(
${config_version_file}
VERSION "${FooBarLib_VERSION}"
COMPATIBILITY ExactVersion
)
#------------------------------------------------------------------------------
# Export '<export_config_name>Targets.cmake' for a build tree
export(
EXPORT ${PROJECT_NAME}Targets
FILE "${CMAKE_CURRENT_BINARY_DIR}/${export_config_name}Targets.cmake"
)
# Configure '<export_config_name>Config.cmake' for a build tree
set(build_config ${CMAKE_BINARY_DIR}/${export_config_name}Config.cmake)
configure_package_config_file(
${export_config_name}Config.cmake.in
${build_config}
INSTALL_DESTINATION "${PROJECT_BINARY_DIR}"
)
#------------------------------------------------------------------------------
# Export '<export_config_name>Targets.cmake' for an install tree
install(
EXPORT ${export_config_name}Targets
FILE ${export_config_name}Targets.cmake
DESTINATION ${FOOBARLIB_INSTALL_CONFIG_DIR}
)
set(install_config ${PROJECT_BINARY_DIR}/CMakeFiles/${export_config_name}Config.cmake)
configure_package_config_file(
${export_config_name}Config.cmake.in
${install_config}
INSTALL_DESTINATION ${FOOBARLIB_INSTALL_CONFIG_DIR}
)
# Install config files
install(
FILES ${config_version_file} ${install_config}
DESTINATION "${FOOBARLIB_INSTALL_CONFIG_DIR}"
)
#------------------------------------------------------------------------------
# Generate package
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY 0)
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}")
# Setting this variable also impacts the layout of TGZ.
set(CPACK_PACKAGING_INSTALL_PREFIX "/opt")
# Setting CPACK_SOURCE_* and CPACK_GENERATOR allow to have "make package" generates
# the expected archive.
# Disable source generator enabled by default
set(CPACK_SOURCE_TBZ2 OFF CACHE BOOL "Enable to build TBZ2 source packages" FORCE)
set(CPACK_SOURCE_TGZ OFF CACHE BOOL "Enable to build TGZ source packages" FORCE)
set(CPACK_SOURCE_TZ OFF CACHE BOOL "Enable to build TZ source packages" FORCE)
# Select generators
if(UNIX AND NOT APPLE)
set(CPACK_GENERATOR "TGZ")
find_program(RPMBUILD_PATH rpmbuild)
if(RPMBUILD_PATH)
list(APPEND CPACK_GENERATOR "RPM")
endif()
elseif(APPLE)
# ...
endif()
include(CPack)
- FooBarLib/FooBarLibConfig.cmake.in
@PACKAGE_INIT@
set(export_config_name "@export_config_name@")
set_and_check(${export_config_name}_TARGETS "${CMAKE_CURRENT_LIST_DIR}/${export_config_name}Targets.cmake")
include(${${export_config_name}_TARGETS})
- useFoo/CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
project(useFoo VERSION 1.2.3)
find_package(FooBarLib REQUIRED)
file(GENERATE OUTPUT foobar-gen CONTENT "<TARGET_FILE:foobar>=$<TARGET_FILE:foobar>\n")
get_target_property(foobar_INCLUDE_DIR foobar INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "foobar_INCLUDE_DIR=${foobar_INCLUDE_DIR}")
get_target_property(imported_location foobar IMPORTED_LOCATION_RELEASE)
get_filename_component(foobar_LIBRARY_DIR ${imported_location} DIRECTORY)
message(STATUS "foobar_LIBRARY_DIR=${foobar_LIBRARY_DIR}")
Output of build.sh
./build.sh
+ set -o pipefail
+++ dirname ./build.sh
++ cd .
++ pwd
+ script_dir=/tmp/stackoverflow-56135785-answer
+ project_name=FooBarLib
+ archive_name=FooBarLib
+ cd /tmp/stackoverflow-56135785-answer
+ rm -rf FooBarLib-build
+ mkdir FooBarLib-build
+ cd FooBarLib-build
+ cmake ../FooBarLib
-- The C compiler identification is GNU 4.8.5
-- The CXX compiler identification is GNU 4.8.5
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Setting build type to 'Release' as none was specified.
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/stackoverflow-56135785-answer/FooBarLib-build
+ make
Scanning dependencies of target foobar
[ 50%] Building CXX object CMakeFiles/foobar.dir/foobar.cpp.o
[100%] Linking CXX shared library libfoobar.so
[100%] Built target foobar
+ make package
[100%] Built target foobar
Run CPack packaging tool...
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: FooBarLib
CPack: - Install project: FooBarLib
CPack: Create package
CPack: - package: /tmp/stackoverflow-56135785-answer/FooBarLib-build/FooBarLib-1.2.3.tar.gz generated.
CPack: Create package using RPM
CPack: Install projects
CPack: - Run preinstall target for: FooBarLib
CPack: - Install project: FooBarLib
CPack: Create package
-- CPackRPM:Debug: Using CPACK_RPM_ROOTDIR=/tmp/stackoverflow-56135785-answer/FooBarLib-build/_CPack_Packages/Linux/RPM
CPackRPM: Will use GENERATED spec file: /tmp/stackoverflow-56135785-answer/FooBarLib-build/_CPack_Packages/Linux/RPM/SPECS/foobarlib.spec
CPack: - package: /tmp/stackoverflow-56135785-answer/FooBarLib-build/FooBarLib-1.2.3.rpm generated.
+ cd /tmp/stackoverflow-56135785-answer
+ rm -rf FooBarLib-install
+ mkdir FooBarLib-install
+ cd FooBarLib-install
+ tar -xvzf ../FooBarLib-build/FooBarLib-1.2.3.tar.gz
opt/
opt/include/
opt/include/foobar.h
opt/lib/
opt/lib/libfoobar.so
opt/lib/cmake/
opt/lib/cmake/FooBarLib/
opt/lib/cmake/FooBarLib/FooBarLibTargets.cmake
opt/lib/cmake/FooBarLib/FooBarLibTargets-release.cmake
opt/lib/cmake/FooBarLib/FooBarLibConfigVersion.cmake
opt/lib/cmake/FooBarLib/FooBarLibConfig.cmake
+ cd /tmp/stackoverflow-56135785-answer
+ rm -rf useFoo-build
+ mkdir useFoo-build
+ cd useFoo-build
+ cpack_install_prefix=/opt
+ cmake -DFooBarLib_DIR=/tmp/stackoverflow-56135785-answer/FooBarLib-install/opt/lib/cmake/FooBarLib/ ../useFoo
-- The C compiler identification is GNU 4.8.5
-- The CXX compiler identification is GNU 4.8.5
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- foobar_INCLUDE_DIR=/tmp/stackoverflow-56135785-answer/FooBarLib-install/opt/include
-- foobar_LIBRARY_DIR=/tmp/stackoverflow-56135785-answer/FooBarLib-install/opt/lib
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/stackoverflow-56135785-answer/useFoo-build
+ cat foobar-gen
<TARGET_FILE:foobar>=/tmp/stackoverflow-56135785-answer/FooBarLib-install/opt/lib/libfoobar.so
+ command -v rpmbuild
+ rpm -qlp /tmp/stackoverflow-56135785-answer/FooBarLib-build/FooBarLib-1.2.3.rpm
/opt
/opt/include
/opt/include/foobar.h
/opt/lib
/opt/lib/cmake
/opt/lib/cmake/FooBarLib
/opt/lib/cmake/FooBarLib/FooBarLibConfig.cmake
/opt/lib/cmake/FooBarLib/FooBarLibConfigVersion.cmake
/opt/lib/cmake/FooBarLib/FooBarLibTargets-release.cmake
/opt/lib/cmake/FooBarLib/FooBarLibTargets.cmake
/opt/lib/libfoobar.so
Only after instrumenting the source of cmake itself was I finally able to track this down.
The export and install commands are both capable of generating cmake files for targets. The export command e.g.:
export(EXPORT foobarLibTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/foobarLibTargets.cmake")
creates a Targets.cmake referencing the build tree.
The install command e.g.:
install(EXPORT foobarLibTargets
FILE foobarTargets.cmake
DESTINATION lib/cmake)
creates a Targets.cmake referencing the relocatable install location.
This is essentially what @J-Christophe meant by saying that two files were installed and the wrong one was picked up.
I had wrongly assumed that the install command was only responsible for installing files and the export command was only responsible for generating them.
The documentation makes sense now
export(EXPORT [NAMESPACE ] [FILE ])
The file created by this command is specific to the build tree and should never be installed. See the install(EXPORT) command to export targets from an installation tree.
The workaround I had previously is no longer necesary. For reference this was to explicitly set the correct location in the package's Config.cmake as in:
set(FOOBAR_VERSION @VERSION@)
@PACKAGE_INIT@
set_and_check(FOOBAR_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@")
set_and_check(FOOBAR_LIBRARY "@PACKAGE_LIBRARY_INSTALL_DIR@/libfoobar.so")
set_and_check(FOOBAR_LIBRARY_DIR "@PACKAGE_LIBRARY_INSTALL_DIR@")
include("${CMAKE_CURRENT_LIST_DIR}/foobarLibTargets.cmake")
# workaround - correct absolute path in the above
# this shouldn't be necessary!
set_target_properties(foobar PROPERTIES
IMPORTED_LOCATION_NOCONFIG "@PACKAGE_LIBRARY_INSTALL_DIR@/libfoobar.so"
)