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 to FooBarLib, 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

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 directory
  • For sake of consistency, use foobarTargets instead of foobarLibTargets
  • <projecname_uc> placeholder used below correspond to the name of the project upper-cased (ABC instead of abc)
  • To allow configuring your project when vendorized along other one, prefer variable with <projecname_uc>_. This means <projecname_uc>_INSTALL_LIBRARY_DIR is better than LIBRARY_INSTALL_DIR.
  • To allow user of the project to configure *_INSTALL_DIR variables, wrap them around if(DEFINED ...)
  • Consistently use variables (e.g LIBRARY_INSTALL_DIR should always be used instead of lib)
  • 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 use PROJECT_VERSION or FOOBAR_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 using install(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 exported foobar 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.
  • 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"
)

Tags:

Cmake