Dependency management in C++ is a bit of a hassle. I have come to like CMake for build and dependency management, despite it’s warts and general negative sentiment in the community. One major feature of CMake is to resolve dependencies. Using CMake packages this can be even comfortable. So here is how to use CMake packages.
Dependency Build Configuration
To build a dependency, you first need to configure the third-party project from its source folder.
mkdir build
cd build
cmake ..
The following is the same, only with a single command:
cmake -B build
This will crate a build
folder in the main folder of
the project/library and will create all CMake configuration
files as well as the IDE project within the build folder.
This is what’s called an in-source build as the build configuration and artifacts are created within the project’s main folder and not in a location outside.
Many libraries have additional configuration options that
allow you to customize how they are build. Make sure to
check their documentation or have a look at their main
CMakeLists.txt
file. Often configuration options are
listed there and are exposed to the CMake GUI tool as well.
Building the Dependency
Build the third-party project/library from the command line, using:
cmake --build build --config release
This will invoke the development environment build tools like msbuild on Windows.
Alternatively, open the created IDE projects (e.g. the Visual Studio solution) and build the targets there.
Installing the Dependency
I would advice to set a custom installation location as the default location on Windows is the “Program Files” folder. This requires administrative rights to install the package.
There are three ways to define the install location for your CMake package:
- Use the
--prefix
argument forcmake --install
, likecmake --install --prefix c:/your/install/abs/path
. - Define the install prefix at configuration time in the first
step using the argument
-DCMAKE_INSTALL_PREFIX=c:/your/install/abs/path
- Set the
DESTDIR
environment variable to your installation location. On Windows, this will typically create a “Program Files” folder at the location you specify. This behavior is kind of weird and I do not find it very useful.
I typically use the prefix argument when installing. Make sure to include the package name as part of the path.
cmake --install build --prefix <absolute-path-to-install-location>
For example:
cmake --install build --prefix c:\users\martin\code\packages\package-name
This will copy all the files into the install location.
Consuming Packages
Make sure to set your CMAKE_PREFIX_PATH
to the location
where you installed your packages. You can either set this as an environment
variable that will work for all your projects or you define CMAKE_PREFIX_PATH
in your project’s CMakeLists.txt
file. The environment variable
has the benefit of being global for your machine, without the
need to contaminate your project configuration that you commit to
your repository.
You can use the package from other CMake projects by using
find_package(packagename CONFIG)
. This will look for
your packages in the default and all specified install locations.
Using find_package with the CONFIG parameter use the packages configuration located in the package folder:
packagename.../lib/cmake/packagename-config.cmake
- or
packagename.../lib/cmake/packagenameConfig.cmake
The folder for the package can have suffixes like version numbers
or build configuration information, like g3log-2.1.2-win64
.
CMake will find the package g3log
in that folder nevertheless as
long as it has the structure of a CMake package.
Example
So for example, a Vulkan “Hello World” project can be configured this way:
project("Hello-Vulkan" VERSION 0.2)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# find packages for dependencies
#
find_package(glm CONFIG REQUIRED) # vector and math library
find_package(g3log CONFIG REQUIRED) # logging library
find_package(glfw3 CONFIG REQUIRED) # window management
find_package(Vulkan REQUIRED COMPONENTS glslc glslangValidator)
add_executable(Hello-Vulkan
src/main.cpp
src/App.h
src/App.cpp
)
target_link_libraries(Hello-Vulkan PUBLIC
glfw
g3log
glm::glm
Vulkan::Vulkan
)
set(glslCompiler "glslangvalidator.exe")
# command to compile example shader files
add_custom_command(OUTPUT Compile_GLSL
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Hello-Vulkan>/shaders"
COMMAND ${glslCompiler} -V "shader.vert" -o "$<TARGET_FILE_DIR:Hello-Vulkan>/shaders/vert.spv"
COMMAND ${glslCompiler} -V "shader.frag" -o "$<TARGET_FILE_DIR:Hello-Vulkan>/shaders/frag.spv"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src/shaders"
)
# custom build target to compile shaders that used the command above
add_custom_target(Compile_Shaders DEPENDS Compile_GLSL)
I have build packages for those dependencies before and installed
them into a custom location. The CMAKE_PREFIX_PATH
environment
variable is set to contain that location.