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:

  1. Use the --prefix argument for cmake --install, like cmake --install --prefix c:/your/install/abs/path.
  2. Define the install prefix at configuration time in the first step using the argument -DCMAKE_INSTALL_PREFIX=c:/your/install/abs/path
  3. 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.

Created by Martin Weber  |  CC-BY-NC-SA This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.  |  Credits