How to write a configurable import target

Hi,
I have a header-only library mylib consisting of the header mylib.h . Inside the header I have a template function. Given a preprocessor macro USE_FAST_LIB the implementation of that function redirects to a third party library “fastlib”. When writing my CMakeLists.txt it looks something like


cmake_minimum_required(VERSION 3.26)
project(mylib LANGUAGES CXX)

option(MYLIB_USE_FAST_LIB “Use fast third-party implementation” OFF)

add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)

if(MYLIB_USE_FAST_LIB)
find_package(fastlib REQUIRED)
target_link_libraries(mylib INTERFACE fastlib::fastlib)
target_compile_definitions(mylib INTERFACE USE_FAST_LIB)
endif()


If you want to install it, you need much more!

I recommend to use FILE_SETS HEADER too.

If you are not experience with CMake, try GitHub - friendlyanon/cmake-init: The missing CMake project initializer

For some reason my e-mail was not formatted correctly here, so I repost my question

Hi,

I have a header-only library mylib consisting of the header mylib.h . Inside the header I have a template function. Given a preprocessor macro USE_FAST_LIB the implementation of that function redirects to a third party library “fastlib”. When writing my CMakeLists.txt it looks something like


cmake_minimum_required(VERSION 3.26 )
project(mylib LANGUAGES CXX)

option(MYLIB_USE_FAST_LIB "Use fast third-party implementation" OFF)

add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
    $<INSTALL_INTERFACE:include>
)

if(MYLIB_USE_FAST_LIB)
    find_package(fastlib REQUIRED)
    target_link_libraries(mylib INTERFACE fastlib::fastlib)
    target_compile_definitions(mylib INTERFACE USE_FAST_LIB)
endif()

My question is: How do I need to write the install rules and package configuration file such that downstream users can use mylib like


# Optionally, before find_package:

set(MYLIB_USE_FAST_LIB ON) # or OFF

find_package(mylib REQUIRED)

target_link_libraries(their_target PRIVATE mylib::mylib)

I.e. they can optionally opt in or out of using the third party library fastlib (because it may not be available on all systems / target-architectues or may not be freely available etc). I know a downstream user can use FetchContent (or CPM) instead of find_package and the above example works. But I have not managed to achieve the same following the cmake tutorial https://cmake.org/cmake/help/latest/guide/importing-exporting/index.html meaning my installed IMPORTED targets have the option “baked-in” depending on what I choose at configuration time of mylib and the option to choose is no longer available to a downstream user. I also dislike the option of providing two targets mylib::mylib_fast and mylib::mylib_regular because it is (i) not scalable with the number of configuration options and (ii) it just pushes the problem off to downstream users who then face the same issue. In my mind there should not be a difference in cmake between importing an interface target with CPMAddPackage / FetchContent or find_package (after all it is the same header file that is being imported and the same configuration options for that header should be exposed to a downstream user).

I would appreciate any help on this

Matthias

Sure, if it makes the discussion easier let’s pretend I used this init project GitHub - friendlyanon/cmake-init-header-only: Example header-only library output of cmake-init

How do I need to modify the install-rules.cmake and the install-config.cmake such that a downstream user of find_package(mylib) can set an option MYLIB_USE_FAST_LIB as described above?

I use this install-config.cmake for my asio project:

include(CMakeFindDependencyMacro)
find_dependency(Threads)

find_package(OpenSSL QUIET)

function(add_asio_module NAME)
    set(ASIO_ROOT @CMAKE_INSTALL_PREFIX@)
    message(STATUS "ASIO_ROOT is: ${ASIO_ROOT}")

    add_library(${NAME})
    target_include_directories(${NAME} PRIVATE ${ASIO_ROOT}/include)
    target_compile_features(${NAME} PUBLIC cxx_std_23)

    set(CPPdefinitions "@CPPdefinitions@")
    if(CPPdefinitions)
        target_compile_definitions(${NAME} PUBLIC ${CPPdefinitions})
    endif()

    # cmake-format: off
    target_sources(
        ${NAME}
        PUBLIC
            FILE_SET modules_public
            TYPE CXX_MODULES
            BASE_DIRS ${ASIO_ROOT}
            FILES ${ASIO_ROOT}/lib/cmake/asio/module/asio.cppm
    )
    # cmake-format: on
    if(OpenSSL_FOUND)
        target_link_libraries(${NAME} PUBLIC OpenSSL::SSL OpenSSL::Crypto)
    endif()
endfunction()

include("${CMAKE_CURRENT_LIST_DIR}/asioTargets.cmake")

if(OpenSSL_FOUND)
    set_target_properties(asio::asio_header PROPERTIES
        # TODO(CK): how to append? INTERFACE_COMPILE_DEFINITIONS "ASIO_HAS_OPENSSL"
        INTERFACE_LINK_LIBRARIES "OpenSSL::SSL;OpenSSL::Crypto;Threads::Threads"
    )
endif()

Thank you for the reply. Just to clarify that I understand this correctly The find_package( OpenSSL QUIET) line will set the OpenSSL_FOUND variable that you use to conditionally add the OpenSSL and Threads targets to the interface of asio::asio_headers?

I have a couple of follow-up questions: Why can’t you use target_link_librarires( asio_headers INTERFACE OpenSSL::SSL) instead of set_target_properties?
Also, I am wondering how this is working when the asio library is installed because in your main CMakeLists.txt you have the line find_package( OpenSSL REQUIRED) and target_link_libraries(asio_header INTERFACE OpenSSL::SSL OpenSSL::Crypto)
So to my understanding the asio library can never be configured without OpenSSL because cmake throws an error if OpenSSL is not available. So when someone installs the asio library the OpenSSL_FOUND variable is always true because it cannot be configured without it? What am I missing?

That is a tricky part of asio

  • as header only, It use dynamic features testing for most options.
  • as static lib, it depends on the static configuration.

This was only a experimental study to help you!

My focus on this MR is the CXX_MODULE.

That is the tricky part of CMake:
I checked the stagedir/lib/cmake/asioTargets-debug.cmake

#----------------------------------------------------------------
# Generated CMake target import file for configuration "Debug".
#----------------------------------------------------------------

# Commands may need to know the format version.
set(CMAKE_IMPORT_FILE_VERSION 1)

# Import target "asio::asio" for configuration "Debug"
set_property(TARGET asio::asio APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(asio::asio PROPERTIES
  IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG "CXX"
  IMPORTED_LOCATION_DEBUG "${_IMPORT_PREFIX}/lib/libasioD.a"
  )

list(APPEND _cmake_import_check_targets asio::asio )
list(APPEND _cmake_import_check_files_for_asio::asio "${_IMPORT_PREFIX}/lib/libasioD.a" )

# Commands beyond this point should not need to know the version.
set(CMAKE_IMPORT_FILE_VERSION)

in stagedir/lib/cmake/asioTargets.cmake you will find this:

# Create imported target asio::asio_header
add_library(asio::asio_header INTERFACE IMPORTED)

set_target_properties(asio::asio_header PROPERTIES
  INTERFACE_COMPILE_DEFINITIONS "ASIO_NO_DEPRECATED;ASIO_DISABLE_BOOST_CONTEXT_FIBER;ASIO_HAS_SNPRINTF"
  INTERFACE_COMPILE_FEATURES "\$<\$<COMPILE_FEATURES:cxx_std_23>:cxx_std_23>;\$<\$<NOT:\$<COMPILE_FEATURES:cxx_std_23>>:cxx_std_20>"
  INTERFACE_LINK_LIBRARIES "OpenSSL::SSL;OpenSSL::Crypto;Threads::Threads"
)

Yes, good question!

Found OpenSSL: /usr/local/Cellar/openssl@3/3.5.0/lib/libcrypto.dylib (found version “3.5.0”)

if not, it would not work with the static lib. I have this runtime dependency for it!

The dynamic is only for the header only library!