Should cmake install an INTERFACE library when linked as PRIVATE dependency ?

Hi folks,

I’m having a hard time understanding how cmake should behave regarding installation of dependencies added with FetchContent so I’m sorry if this is a noob question, but I couldn’t find a satisfying answer so far.

I’m working with the C++ library xsimd and we’re having a discussion on how should behave the library in case of installing.

We’re including the library using FetchContent :

include(FetchContent)

FetchContent_Declare(
  xsimd
  GIT_REPOSITORY git@github.com:xtensor-stack/xsimd.git
  GIT_TAG 11.1.0
  GIT_SHALLOW ON
)

FetchContent_MakeAvailable(xsimd)

The library itself is an INTERFACE declared header-only library.

From what I understand, if the xsimd library is declared as a PRIVATE dependency of my project, it should not be installed alongside my project (since its header-only) when installing. Whereas with PUBLIC/INTERFACE it should.
However with this library, it is always installed alongside the parent project.

Regarding current modern best practices, how do you think you it behave ?

If you think this is a bug, have you any ideas why does it behaves like that ?

Here is the CMakeLists.txt


cmake_minimum_required(VERSION 3.8)
project(xsimd)
option(XSIMD_REFACTORING ON)

set(XSIMD_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include)

# Versioning
# ==========

file(STRINGS "${XSIMD_INCLUDE_DIR}/xsimd/config/xsimd_config.hpp" xsimd_version_defines
     REGEX "#define XSIMD_VERSION_(MAJOR|MINOR|PATCH)")
foreach(ver ${xsimd_version_defines})
    if(ver MATCHES "#define XSIMD_VERSION_(MAJOR|MINOR|PATCH) +([^ ]+)$")
        set(XSIMD_VERSION_${CMAKE_MATCH_1} "${CMAKE_MATCH_2}" CACHE INTERNAL "")
    endif()
endforeach()
set(${PROJECT_NAME}_VERSION
    ${XSIMD_VERSION_MAJOR}.${XSIMD_VERSION_MINOR}.${XSIMD_VERSION_PATCH})
message(STATUS "xsimd v${${PROJECT_NAME}_VERSION}")

# Build
# =====

set(XSIMD_HEADERS
[...]
${XSIMD_INCLUDE_DIR}/xsimd/xsimd.hpp
)

add_library(xsimd INTERFACE)

target_include_directories(xsimd INTERFACE
    $<BUILD_INTERFACE:${XSIMD_INCLUDE_DIR}>
    $<INSTALL_INTERFACE:include>)

OPTION(ENABLE_XTL_COMPLEX "enables support for xcomplex defined in xtl" OFF)
OPTION(BUILD_TESTS "xsimd test suite" OFF)

if(ENABLE_XTL_COMPLEX)
    find_package(xtl 0.7.0 REQUIRED)
    target_compile_features(xsimd INTERFACE cxx_std_14)
    target_compile_definitions(xsimd INTERFACE XSIMD_ENABLE_XTL_COMPLEX=1)
    target_link_libraries(xsimd INTERFACE xtl)
else()
    target_compile_features(xsimd INTERFACE cxx_std_11)
endif()

if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(test)
endif()

OPTION(BUILD_BENCHMARK "xsimd benchmarks" OFF)
if(BUILD_BENCHMARK)
    add_subdirectory(benchmark)
endif()

OPTION(BUILD_EXAMPLES "xsimd examples" OFF)
if(BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

# Installation
# ============

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(JoinPaths)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

install(TARGETS xsimd
        EXPORT ${PROJECT_NAME}-targets)

# Makes the project importable from the build directory
export(EXPORT ${PROJECT_NAME}-targets
       FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Targets.cmake")

install(DIRECTORY ${XSIMD_INCLUDE_DIR}/xsimd
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# GNUInstallDirs "DATADIR" wrong here; CMake search path wants "share".
set(XSIMD_CMAKECONFIG_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" CACHE STRING "install path for xsimdConfig.cmake")

configure_package_config_file(${PROJECT_NAME}Config.cmake.in
                              "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
                              INSTALL_DESTINATION ${XSIMD_CMAKECONFIG_INSTALL_DIR})

# xsimd is header-only and does not depend on the architecture.
# Remove CMAKE_SIZEOF_VOID_P from xtensorConfigVersion.cmake so that an xtensorConfig.cmake
# generated for a 64 bit target can be used for 32 bit targets and vice versa.
set(_XTENSOR_CMAKE_SIZEOF_VOID_P ${CMAKE_SIZEOF_VOID_P})
unset(CMAKE_SIZEOF_VOID_P)
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
                                 VERSION ${${PROJECT_NAME}_VERSION}
                                 COMPATIBILITY SameMajorVersion)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
              ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
        DESTINATION ${XSIMD_CMAKECONFIG_INSTALL_DIR})
install(EXPORT ${PROJECT_NAME}-targets
        FILE ${PROJECT_NAME}Targets.cmake
        DESTINATION ${XSIMD_CMAKECONFIG_INSTALL_DIR})

configure_file(${PROJECT_NAME}.pc.in
               "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc"
                @ONLY)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc"
        DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig/")

INTERFACE libraries may have link requirements (e.g., a header-only C++ library that wraps some C library still needs to link to the C library when used). If you know that you don’t need it in the install, $<INSTALL_INTERFACE> will get you that. If you know you don’t need it in any export, $<BUILD_LOCAL_INTERFACE> is available in CMake 3.26+ as an further enhancement.

if (CMAKE_VERSION VERSION_LESS "3.26")
  set(iface_type "INSTALL_INTERFACE") # still need to make the target available to `export()`
else ()
  set(iface_type "BUILD_LOCAL_INTERFACE")
endif ()

target_link_libraries(tgt PRIVATE "$<${iface_type}:xsimd>")
1 Like

Thanks a lot for the reply ! I finally had time to test it.

I tested what you suggested, but I had an issue that I kinda expected.

target_link_libraries(
  adawata
  PRIVATE $<INSTALL_INTERFACE:xsimd>
)

When doing this the library is not available at build time for my library. I though of doing the exact opposite (I need it a build time, not install time) by using $<BUILD_INTERFACE:xsimd> but it ends up installing xsimd anyway.

Err, yeah. BUILD_INTERFACE, not INSTALL_INTERFACE. Does BUILD_LOCAL_INTERFACE work?

No, BUILD_LOCAL_INTERFACE doesn’t work either

When you say “it ends up installing xsimd anyways”, what do you mean specifically?

Sorry, I’ll try to be more clear.

When installing my lib, xsimd is installed too, in the same folder, even though I don’t need it to.

When you check install/include you can see the xsimd folder with all the headers :

╰─➤  tree install -L 2
install
├── include
│   ├── adwt
│   ├── cppitertools
│   ├── gsl
│   └── xsimd                      <-------- should not be here
└── lib
    ├── cmake
    ├── libadawata.a
    ├── libsamplerate.a
    └── pkgconfig

Oh, xsimd probably has install rules you need to suppress there. No _INTERFACE genex will undo that request.

Ok thanks :slight_smile: That what I ended up thinking. However I could not identify what is causing this in the install rules of xsimd.

In xsimd’s CMakeLists.txt (I copied the whole CMakeList.txt in the first message) I suspect this :

install(DIRECTORY ${XSIMD_INCLUDE_DIR}/xsimd
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

Would you have a tip or some resources to read on how to make it more “proper” ?

The thing is, I don’t want to completely disable the feature to install xsimd with my library, because some users might need it when linking to it publicly.

Just wrapping it up in an if (NO XSIMD_NO_INSTALL) conditional that lets you set set(XSIMD_NO_INSTALL 1) before including your vendored copy.

Are there no cmake features that would allow dealing with it in a more “automatic” way ? (Sorry if this is a repetitive question, I’m trying to wrap my head around cmake’s modern features)

Install rules are imperative; there’s no “modern” usage-requirement way to make them “go away”.

Ok, thanks a lot for your time !

For the record, I found a new way using FetchContent and the EXCLUDE_FROM_ALL flag, thanks to this StackOverflow post.

For short, with cmake <3.28 you’d use :

include(FetchContent)

FetchContent_Declare(
  xsimd
  GIT_REPOSITORY git@github.com:xtensor-stack/xsimd.git
  GIT_TAG 11.1.0
  GIT_SHALLOW ON
)

FetchContent_GetProperties(xsimd)
if(NOT xsimd_POPULATED)
  FetchContent_Populate(xsimd)
  add_subdirectory(${xsimd_SOURCE_DIR} ${xsimd_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()

And with cmake >= 3.28 we’ll be able to use this syntax :

include(FetchContent)

FetchContent_Declare(
  xsimd
  GIT_REPOSITORY git@github.com:xtensor-stack/xsimd.git
  GIT_TAG 11.1.0
  GIT_SHALLOW ON
  # Other content options...
  EXCLUDE_FROM_ALL
)

FetchContent_MakeAvailable(xsimd)