CMake import targets and find package via module file

Introduction

When working with external libraries in CMake, we often need to reference pre-built executables or libraries without building them from source. This is where imported targets come in. They allow us to link against external libraries seamlessly.

In this post, I’ll demonstrate how to use IMPORTED targets to include external libraries. The topic of IMPORTED targets paves the way for me to explain how to write a Find<Package>.cmake module that lets us import an external library to any project.

Imported target

An Imported target refers to an already build external executable or library. Therefore, it won’t be built, it will only be used for linking or running commands. In the example below, to build an executable, an external shared library, geo is imported then it is used for linking:

cmake_minimum_required(VERSION 3.23)
project(myExample LANGUAGES CXX)

add_library(geo SHARED IMPORTED GLOBAL)
set_target_properties(geo PROPERTIES
            # default
             IMPORTED_LOCATION "path/to/geometryInstall_release/bin/geo.dll"
             IMPORTED_IMPLIB "path/to/geometryInstall_release/lib/geo.lib"
             # Config specific
             IMPORTED_LOCATION_DEBUG "path/to/geometryInstall_debug/bin/geo.dll"
             IMPORTED_IMPLIB_DEBUG "path/to/geometryInstall_debug/lib/geo.lib"
             IMPORTED_CONFIGURATIONS "RELEASE;DEBUG"
             )             

target_include_directories(geo INTERFACE "path/to/geometryInstall_release/include" )

add_executable(myApp app.cpp)
target_link_libraries(myApp PRIVATE geo)

add_custom_command(
  TARGET myApp
  POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:geo> $<TARGET_FILE_DIR:myApp>
  )

Important points are:

  • add_library(... IMPORTED GLOBAL): means treat this as imported library and also makes it global, i.e. make it available in the parent directories.

  • IMPORTED_LOCATION: the path to an executable, shared library (.dll, .so), static library (.a) or module. It is the default path for all configurations: debug, release, and so on.

  • IMPORTED_IMPLIB: on Windows, is the path to .lib accompanying a dll file. It is the default for all configurations: debug, release, and so on.

  • IMPORTED_LOCATION_DEBUG: If you have a Debug/Release/RelWithDebInfo/minSizeRel version of the imported library, you can import them separately. Therefore, if a target compiled in debug. it links to debug version of this library.

  • IMPORTED_CONFIGURATIONS: declaring the configurations for the imported target.

  • target_include_directories: This lets projects linking to geo library, use its header files.

  • add_custom_command(TARGET myApp ...): to copy the dll file into the executable directory.

Another important property is IMPORTED_LINK_INTERFACE_LIBRARIES that is set to the list of the libraries that should be linked when using an imported target.

Find module by Find package

In CMake, Find_package() is used to to import an installed library. It has three modes: module, config and FetchContent redirection. In config mode, <lowercasePackageName>-config.cmake or <PackageName>Config.cmake file is searched. In this post, I explained how to create a config file. It is also possible to redirect find_package() to use a fetched content. I explained FetchContent here.

Here, I focus on the module mode of find_package() when we provide a Find<PackageName>.cmake file. This is for a situation that the installed external project doens’t have a config file. The module file can be provided by operating system (e.g. Linux) or a third party. Here, I want to explain how we can straightforwardly write one to easily import an installed external project as a target.

I already created a sample file, FindGeometry.cmake which loads geometry library:

# Find include directories by searching provided paths for geometry.h
find_path(Geometry_INCLUDE_DIR
    NAMES geometry.h
    PATHS "path/to/myInstallRelease/include/geometry"
)

# I want include directories be relative to include.
if (Geometry_INCLUDE_DIR)
    get_filename_component(Geometry_INCLUDE_DIR ${Geometry_INCLUDE_DIR} DIRECTORY)
endif()

# Looks for geo.lib
find_library(Geometry_LIBRARY
    NAMES geo
    PATHS "path/to/myInstallRelease/lib"
)

find_file(Geometry_DLL
    NAMES geo.dll
    PATHS "path/to/myInstallRelease/bin"
)

# Create an imported target if the library was found
if (Geometry_INCLUDE_DIR AND Geometry_LIBRARY AND Geometry_DLL)
    add_library(geo SHARED IMPORTED GLOBAL)
    set_target_properties(geo PROPERTIES
        IMPORTED_LOCATION "${Geometry_DLL}"
        IMPORTED_IMPLIB "${Geometry_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${Geometry_INCLUDE_DIR}"
    )
endif()

# Provide standard CMake package variables
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Geometry
    FOUND_VAR Geometry_FOUND
    REQUIRED_VARS Geometry_INCLUDE_DIR Geometry_LIBRARY Geometry_DLL
)

# make the variables advanced to not shown in CMake GUI
mark_as_advanced(Geometry_INCLUDE_DIR Geometry_LIBRARY Geometry_DLL)

Using find_path(), find_library() and find_file() is a good practice to have the module file cross-platform and to provide multiple search paths. But if you are writing for one OS and a simple case, you can set the related variables directly. Finally if everything goes well, Geometry_FOUND will be set.

Now, in another project, I put FindGeometry.cmake file into cmake_modules directory and load it as:

cmake_minimum_required(VERSION 3.23)
project(myExample LANGUAGES CXX)

set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake_modules" ${CMAKE_MODULE_PATH})

find_package(Geometry REQUIRED)

add_executable(myApp app.cpp)
target_link_libraries(myApp PRIVATE geo)

add_custom_command(
  TARGET myApp
  POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:geo> $<TARGET_FILE_DIR:myApp>
  )
Tags ➡ C++

Subscribe

I notify you of my new posts

Latest Posts

Comments

0 comment