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>
)