Getting Started with CMake - 3 - Use External Project
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:
- Directly download source code
- Install and
find_package
- 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:
- https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html#find-modules
SDL is one of those modules, so one can callfind_package(SDL)
- https://cmake.org/cmake/help/latest/module/FindSDL.html
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.