Menu Share

How to manage dependencies with CMake

by | Last Updated:
ANSI C, C++

CMake is the most used code project tool for C/C++ . It is widely used but also has a steep learning curve for a beginner. One of the core things in any code project is managing dependencies as it will be very hard for small teams of people to write and know every single topic. Usually there are ready to use libraries out there and you just need to find them.

Lets say for example that you would like to build a custom game engine. You could tackle all the problems – invent graphics, mathematics, physics, etc. But it would be way easier to start from public libraries that already solve these problems.

Table of Contents

Before you begin

Keep in mind thtat this is the harder approach in some way to managing dependecies. Check out my other tutorial on how to manage them using a package manager.

The approach described in this article is useful when you want to secure your dependencies will always be the correct version and you want to compile them alongside your project. It is also mainly useful when dependencies are also managed with CMake too but it is not required.

I expect that you are already familiar with how CMake works and how to write code in C/C++ .

Subdirectories

My approach when managing dependencies without a tool like vcpkg is to add them as subdirectories. Keep in mind that there are other ways to use packages and managing dependencies but I like this approach as it puts all dependecies at the same relative level in your project and it is more clear and easier for initial compilation.

In CMake projects you can split your code into chunks (libraries) and then add executables (application or tests). Read more on those in my CMake targets article before you proceed. You would be wise to do that by seperating them into their own directories. The command add_subdirectory in CMake language does only one thing – takes the directory path relative to the current CMakeLists.txt directory and executes the CMakeLists.txt in that directory. So in thery if you download your dependency as a subdirectory to your project you can add it and then link the library to your executable.

You can also split your code the same way into consumable modules and even make different repositories for each module so that you can reuse them in the future.

Fetching the dependency

There are a few aproaches to taking your dependencies for this approach. You can use the “git submodule” or “git subtree” sets of commands if you are under Git SVN.

Git Submodule

The git submodule approach will make a directory in your git repository to reference another repository available in the internet. Git then provides a whole set of commands to update this reference and download the contents of that repository into the directory.

What is most useful about the git submodule approach is that it doesn’t keep track of the contents of the repository it just keeps a reference. You can also easily commit back to the initial repository.

Use this approach with your own managed repositories because you may find useful to make imrpovements on them and commit them back directly to their main repo.

Read more: Git Submodules official documentation

Git Subtree or Zip download

I prefer the git subtree as it is easier to update and allows you to modify dependencies without pushing anything back to the original repository. If you’re using another kind of source control you can just download your dependencies as an archive (zip) and then extract them relative to your project.

What git subtree essentially does is to copy the contents of another repository into your own. It will automatically make a commit when you do that. Alternatively it is the same as downloading a zip file and extracting it into your own project then commiting all the files.

This is more useful when working with external projects as you never know what could happen with the original repository and whatever happens you would have a copy of their code. If the project that you use gets taken down you could continue to support your own copy / fork of the code and your project would build without any problems.

Read more: Git Subtrees – Atlassian article

Project sturcture

Because this is a broad topic and I would like to keep the article short I would assume that you already know how to download a repo and extract it in the correct place.

My general approach is to first create a library called packages. It will serve as a singular point of combination of all your dependencies. This library would be in a subdirectory called Packages relative to the root of your project. So I do this by creating a folder called Packages and adding a CMakeLists.txt in it. I also add a Packages.h and Packges.cpp so that I also add all dependency includes in the Packages.h file later. Then all dependencies will go as subdirectories to that.

root
├── Packages/
│ ├── ...dependency subdirectories...
│ ├── include/
│ │ └── Packages.h
│ ├── src/
│ │ └── Packages.cpp
│ └── CMakeLists.txt
├── Main/
│ ├── src/
│ │ └── ...game files...
│ └── CMakeLists.txt
└── CMakeLists.txt

The CMakeLists.txt file in the Packages directory would then contain something like this:

project(Packages)

# example raylib library
# add_subdirectory(raylib)

add_library(Packages STATIC include/Packages.h src/Packages.cpp)

# example linking raylib
# target_link_libraries(Packages raylib)

target_include_directories(Packages
                           PUBLIC
                           $<INSTALL_INTERFACE:include>
                           $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
                           PRIVATE
                           ${CMAKE_CURRENT_SOURCE_DIR}/src
                           )

In the commented code you can see an example of how to link raylib library into the packages library. This would usually be a static link but some libraries have other optional configurations.

How your main target should look like?

Also to clarify on how the packages project is used later on here is an example executable that will use the Packages library:

project(Main LANGUAGES CXX)

add_executable(Main src/main.cpp)

target_link_libraries(Main PUBLIC Packages)

target_include_directories(Main
        PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
        )

As you can see in the example we can then keep it quite simple. It wouldn’t matter how many dependencies we include in the Packages project as the main will only include one target and that will keep it simpler.

Okay… but how to include dependencies?

And here is the main problem. There is no universal way ! C and C++ projects are two scattered on different build systems: some use CMake, some use Make, some use SCons, etc. You get the point. What is true about all of them is that they have headers and they have source files that build into a library. For some of them it would be as easy as adding a subdirectory for others as hard as implementing the whole build system for them.

I am building my own game engine and I will show you some of the most common library includes that you will have to use if you decide to make a custom engine yourself.

Raylib

Raylib for example is pretty easy. You have to follow the documention in Ray’s repository but more or less you just need to add:

# ...

add_subdirectory(raylib)

# ... 

target_link_libraries(Packages raylib)

OpenGL / Glad

Glad project doesn’t come with a repository. You generate your API headers yourself for what you want to use from the OpenGL specification. To do this you head on to this site https://glad.dav1d.de/ and generate yourself an API. You will end up with a zip with two or tree files that I put into a directory called glad.

glad
├── src/
│ └── glad.c
├── include/
│ ├── glad/
│ │ └── glad.h
│ └── KHR/
│   └── khrplatform.h
└── CMakeLists.txt

I then also throw in a CMakeLists.txt file in there and configure a new project of my own:

project(glad LANGUAGES CXX)

add_library(glad STATIC
        src/glad.c
        include/glad/glad.h
        include/KHR/khrplatform.h
        )


target_include_directories(glad
        PUBLIC
        $<INSTALL_INTERFACE:include>
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
        )

Then to add it it would be as easy as:

# ...

# Some magic required on linux btw if you're working with OpenGL
find_package(OpenGL REQUIRED)

add_subdirectory(glad)

# ... 

target_link_libraries(Packages glad ${OPENGL_LIBRARIES}
)

So we covered the two extreme cases – you have to just include a directory to you have to create another cmake project just out of the headers. Wasn’t that hard heh?

ImGui

So here is a more sensitive case. If you work with external vendor code you would generally not want to write CMakeLists.txt files inside the directory that you copy the external code and systems in. This makes it a bit tricky to include your build systems into them if they don’t use CMake. There are two general approaches here – clone and support your own fork of their repository and update it periodically.

Or just create your own targets from the files outside. If we download imgui into a directory imgui for example it would look like this in the Packages/CmakeLists.txt :

add_library(imgui STATIC
            imgui/imgui.h
            imgui/imconfig.h
            imgui/imgui_internal.h
            imgui/imstb_rectpack.h
            imgui/imstb_textedit.h
            imgui/imstb_truetype.h
            imgui/imgui.cpp
            imgui/imgui_demo.cpp
            imgui/imgui_draw.cpp
            imgui/imgui_widgets.cpp
            imgui/imgui_tables.cpp
            imgui/backends/imgui_impl_glfw.h
            imgui/backends/imgui_impl_glfw.cpp
            imgui/backends/imgui_impl_opengl3.h
            imgui/backends/imgui_impl_opengl3.cpp
            )

target_link_libraries(imgui glad glfw)

target_include_directories(imgui
                           PUBLIC
                           $<INSTALL_INTERFACE:imgui>
                           $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/imgui>
                           )

# ... 

target_link_libraries(Packages imgui)

Lua

Lua for example is a makefile project. You can do the same but not pollute your main Packages/CMakeLists.txt but create the following structure like we did with glad:

glad
├── src/
│ └── ...clone your lua here...
└── CMakeLists.txt

I won’t lie to you. For some of these you would have to dive into the C/C++ code and get your hands dirty to understand how to compile them correctly. I took the official mirrored repository for lua and tried to compile first by using the following line to gather all the source files into a cmake variable:

file(GLOB Lua_Sources src/*.c)

add_library(lua STATIC Lua_Sources)

target_include_directories(lua
                           PUBLIC
                           $<INSTALL_INTERFACE:src>
                           $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
                           )

… It will generate projects just fine but then throw an error at me. And turns out the error was about duplicate symbols being defined. Check my article on C/C++ compilation where I mention most common compilation errors. What I found out was that the library can be compiled only with one source file called onelua.c So I changed my script to look like this:

project(lua LANGUAGES C)

add_library(lua STATIC src/onelua.c)

target_include_directories(lua
                           PUBLIC
                           $<INSTALL_INTERFACE:src>
                           $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
                           )

… Aaaand again I got a fail. This time I got that my main file wouldn’t want to compile because the main function was a duplicate. Then I had to search deeper into the Lua C code to find out that there is a definition required to build lua as a library and not an interperter. The line to do this is:

target_compile_definitions(lua PRIVATE MAKE_LIB)

So this is the process of including lua into CMake project. The rest is the same as with the previous:

# ...

add_subdirectory(lua)

# ... 

target_link_libraries(Packages lua)

Assimp

Some projects are made in CMake but are so complex that they expose CMake options that you need to toggle when building the library. This is easy if you’re building the project separate but requires some magic if you’re doing it when adding the referenced project as a subdirectory. The example with Assimp (which is a 3D model loading library) is that you can define what file formats and operations you would like to include for loading models. You may for example need to build only a FBX loader:

set(BUILD_SHARED_LIBS FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_NO_EXPORT TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_TESTS FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_INSTALL_PDB FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_ZLIB TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_ASSIMP_TOOLS FALSE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_COLLADA_IMPORTER TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_OBJ_IMPORTER TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_FBX_IMPORTER TRUE CACHE BOOL "x" FORCE)
set(ASSIMP_BUILD_BLEND_IMPORTER TRUE CACHE BOOL "x" FORCE)
add_subdirectory(assimp)

# ...

target_link_libraries(Packages assimp)

Options are pretty hard to set and because of that we need to force them to be a certain value before we add the relevant subdirectory. If the name is too generic like BUILD_SHARED_LIBS you may also want to unset it so that it doesn’t affect other packages that you include.

Some simpler examples

I’m also going to give some awesome libraries that represent simpler examples and their repositories: GLFW , glm , EnTT , sol2 , spdlog , fmt , easy_profiler , OpenAL-soft :

# you may also care to set the options
# LIBTYPE to STATIC
# ALSOFT_UTILS to FALSE
# ALSOFT_EXAMPLES to FALSE
# ALSOFT_BACKEND_DSOUND to FALSE if you are building with Github Actions on Windows
add_subdirectory(openal-soft)

add_subdirectory(entt)

add_subdirectory(glfw)

add_subdirectory(glm)

add_subdirectory(easy_profiler)

add_subdirectory(fmt)

add_subdirectory(spdlog)

add_subdirectory(sol2)

target_link_libraries(Packages
                      glfw
                      glm
                      OpenAL
                      assimp
                      easy_profiler
                      EnTT
                      fmt
                      spdlog
                      lua
                      sol2
                      )

How do you know…

Every project is different. The hard thing is that there is no universal way that everyone does it and as CMake is actually a capable programming language by itself you may need to dig deep to know how to include every project that you may want to depend on. There are usually two important questions that you would need to answer yourself:

How do you know what to put inside target_link_libraries

This is tricky – you would have to search inside the dependency project CMakeLists.txt file to find the place where a command to add_library is called. Then whatever is the target name that will be what you need to use in the target_link_libraries.

How do you know what options are available

You need to read carefuly the CMakeLists.txt file of your dependency to see every place where a variable controls what gets compiled. Sometimes you may find handy documentation in the repository that would instruct you what to do but that is rare. Look for all SET() commands and all OPTION() commands. Sometimes you may even need to go as deep as to see what triggers certain preprocessor definitions in the C/C++ code.

One more way to add dependencies

If your dependencies are based on CMake there is also one more way to include them in your project. That is through “FetchContent”. You can read more about it on the modern cmake site. Here is an example code of how to include Catch2 library for testing:


include(FetchContent)

FetchContent_Declare(
        catch
        GIT_REPOSITORY https://github.com/catchorg/Catch2.git
        GIT_TAG        v2.13.0
)

# CMake 3.14+
FetchContent_MakeAvailable(catch)

This approach though just simplifies some of the above approaches. Internally it is equivalent to downloading the project and adding it as a subdirectory. It will just be done inside your build directory.

Conclusion

This was a rather long article and I hope I didn’t scare you away. It is rather complex when it comes to dependencies sometimes but it really depends on the project you’re including. This does save time in the long run as including already solved problems you won’t have to implement them yourself (and that may sometimes require too in depth knowledge).

Leave a comment