Getting Started with CMake - 3 - Use External Project

Jeff posted on  (updated on )

In a complex C/C++ project, it's very common to use external dependencies. C/C++ doesn't come with official package manager like cargo(Rust) or Pip(Python), so it's up to the developer to decide how to obtain and use external dependencies in their C/C++ projects.

CMake is one of the most popular C/C++ build systems, and it provides extensive support for using external dependencies. Thus the process has been greatly simplified. In this article, we are going to see 3 most common ways of using external dependencies in a CMake project:

  1. Directly download source code
  2. Install and find_package
  3. Use dependency's CMakeLists.txt

You can also check CMake official guide on Using Dependencies

1. Directly use external project's source code

The first method of using an external C/C++ project is, well, simply download all of its source codes and build it as part of your application. You will want to use this method if the dependency doesn't provide CMake files.

To use such a external project, just build the downloaded source code and create an internal library (add_library). And later link this library to your final binary/library by using target_link_libraries.

Example of directly use source code

imgui is probably one of the best demonstration of such usage. Due to the natural of that project, it doesn't provide a CMake file.

Instead, it's best to download the source code, and create your own CMakeLists.txt to config/compile relevant source files. For example, in my project Intel 8080 emulator, I add imgui as a git submodule, then build it into a library called imgui, and later link my final executable to it.

2. Install and find_package

Another popular way to use external projects is find_package. This typically works for external dependencies that are installed on your system, either by using your OS's package manager, or manually download the source code -> compile -> install, then you use find_package to load the project-provided CMake config file to use that external project in your project.

find_package is a command provided by CMake.

For a external project "FooBar" to support find_package, it must install a configuration file during installation (usually called foobar-config.cmake), so later find_package(FooBar) can read this configuration file, where the developer of "FooBar" tells CMake how to compile and build that library.

Example of find_package

We will see an example of using SDL2 in a project.
First, you install SDL2 through package manager: sudo apt install sdl2
Then, in your CMake project's CMakeLists.txt file, find and link SDL2.

# First, find
find_package(SDL2 REQUIRED)

# Second, link
target_link_libraries(emulator PRIVATE
	SDL2::SDL2
	...)

Let's examine how this works under the hood.

Official doc about using SDL2 in CMake https://github.com/libsdl-org/SDL/blob/release-2.30.1/docs/README-cmake.md

By installing SDL2, a config file called sdl2-config.cmake will be provided. And when we call find_package(SDL2) in our project, CMake will read and execute commands in this file.

We can see in sdl2-config.cmake, it creates a target library called SDL2::SDL2 here:
https://github.com/libsdl-org/SDL/blob/release-2.30.1/sdl2-config.cmake.in#L140

add_library(SDL2::SDL2 SHARED IMPORTED)

So later in our project, we could call target_link_libraries(emulator PRIVATE SDL2::SDL2 ....

How does find_package know where to find config file?

cmake -D CMAKE_FIND_DEBUG_MODE=ON .. prints out the search path

CMake comes with pre-defined config files for some popular modules:

However! Note the different version number here. In this project, when we use find_package(SDL2 REQUIRED), these CMake pre-defined modules are not used. Instead, CMake will search a set of directories to locate the config file.

Q: Why pre-defined module doesn't work in this case

If we use find_package(SDL REQUIRED) it would fail to build. Why?

Answer: Pre-defined module is for SDL1. Not for newer versions.

Q: What's in the config file?

With cmake -D CMAKE_FIND_DEBUG_MODE=ON .., we can see CMake found the config file at following location:

/opt/homebrew/lib/cmake/SDL2/sdl2-config.cmake

Clearly this location is provided by brew (a package manager on macOS). sdl2-config.cmake is generated from https://github.com/libsdl-org/SDL/blob/release-2.30.1/sdl2-config.cmake.in#L139-L149. Earlier we saw that this config file creates a target SDL2::SDL2 where we can use in our project.

Q: How does CMake know to search /opt/homebrew/lib/cmake for config file?

CMake iterate through all prefix here: CMAKE_SYSTEM_PREFIX_PATH, and construct a search path by using procedures explained here: https://cmake.org/cmake/help/latest/command/find_package.html#config-mode-search-procedure

CMAKE_SYSTEM_PREFIX_PATH: /Library/Developer/CommandLineTools/SDKs/MacOSX14.2.sdk/usr;/opt/homebrew;/usr/local;/usr;/;/opt/homebrew/Cellar/cmake/3.28.3;/usr/local;/usr/X11R6;/usr/pkg;/opt;/sw;/opt/local

Use dependency's CMakeLists.txt

This approach will also first download the source code of external dependency, then we utilize the CMakeLists.txt that comes with the project by using add_subdirectory.

Example

Let's demonstrate how to use flecs in this approach.

No need to learn what flecs is, just look at how CMakeLists.txt is used here.

We can see that this project comes with a root level CMakeLists.txt, and in that file target flecs is defined:

add_flecs_target(flecs SHARED)

So in my project, I could first add flecs as a git submodule (essentially download all of it's source code), then in CMakeLists.txt:

add_subdirectory(3rdparty/flecs)

This will read the dependency's root-level CMakeLists.txt, instead of foobar-config.cmake, to create target we want to link against:

target_link_libraries(model_viewer PRIVATE flecs::flecs)

I personally prefer this approach, because all the dependencies are installed with a recursive clone of single git repository, no need to manually install packages (like apt install flecs).

However this only works if the project provides such root level CMakeLists.txt file.