My previous CMake tutorial taught the basics: how to compile multiple source files into one program. That's great and all, but you're almost guaranteed to need third-party libraries when writing something that's actually useful. You don't want to be writing all that code yourself. So, today we're going to tackle linking your program to third-party libraries with CMake as the build system.

Want to get up to speed with CMake quickly? Click here...

Don't use find_package()

At first glance find_package() looks like it's made for the job. However, it feels like a black box that magically finds C/C++ libraries on your hard drive. The magic fades when it fails to find the library you need and spits out an error instead. At that point rookie developers get a feeling of overwhelm while scratching their heads and wondering "what now?"

Use FetchContent Instead

CMake has a newer feature that solves this problem. It can download missing packages from the internet instead of spitting out an error. You just need to give it the URL of the package/library you need.

Linking to RayLib (and Other Libraries)

I'm going to link to a library called RayLib for this example. It's a great beginner friendly game/multimedia library that allows you to open a window and draw stuff with just a few lines of code, just like this:

#include "raylib.h"

int main(int argc, const char **argv) {
    // Initialization
	const int screenWidth = 1280;
    const int screenHeight = 768;
    const char *windowTitle = "Hello world!";
    const char *message = "Hello world! It's great to be here.";
    const int fontSize = 40;
    const float msgSpacing = 1.0f;

    InitWindow(screenWidth, screenHeight, windowTitle);

    // NOTE: The following only works after calling InitWindow() (i.e,. RayLib is initialized)
    const Font font = GetFontDefault();
    const Vector2 msgSize = MeasureTextEx(font, message, fontSize, msgSpacing);
    const Vector2 msgPos = Vector2{(screenWidth - msgSize.x) / 2, (screenHeight - msgSize.y) / 2};

    SetTargetFPS(60);

    // Main loop
    while(!WindowShouldClose()) {

        // Update the display
        BeginDrawing();
            ClearBackground(RAYWHITE);
            DrawTextEx(font, message, msgPos, fontSize, msgSpacing, RED);
        EndDrawing();
    }

    // Cleanup
    CloseWindow();
	
	return 0;
}

Fetching, and Building Dependencies (Such As RayLib) with CMake

As mentioned earlier, we're going to use FetchContent. The first step is to tell CMake you want to use the FetchContent module. Add the following to your project's CMakelists.txt file:

# Dependencies
include(FetchContent)

Next, we "declare" that we want RayLib as follows:

set(RAYLIB_VERSION 4.5.0)
FetchContent_Declare(
    raylib
    URL https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_VERSION}.tar.gz
    FIND_PACKAGE_ARGS ${RAYLIB_VERSION} EXACT
)

There are a few things going on in the code above. For starters, we specifically want RayLib 4.5.0. It's good practice to specify which version you want. Otherwise, you might be using version 4.5.0, and your colleague has version 4.2.1. Whether the program works properly can depend on which of you compiled it. Not good. It's much better to make sure that everyone is using the same version so that everyone has the same code and same behaviour.

Next, pay close attention to the URL, because it's the key to downloading packages from GitHub (and other source code repositories). The URL is in the format:

https://{repository_base_url}/archive/refs/tags/{version_number_or_tag}.tar.gz

Most projects will tag their releases by version number, so the URL above is a direct link to download that version.

The declaration above sets up the package details, but won't actually do anything. That's FetchContent_MakeAvailable()'s job. However, first we need to enable/disable some of the RayLib library's features. We don't need the examples, so we can disable that by setting RayLib's BUILD_EXAMPLES variable:

set(BUILD_EXAMPLES OFF CACHE INTERNAL "")

Library developers: please prefix your configuration variables (e.g., RAYLIB_BUILD_EXAMPLES) so that they're unique and don't clash with other libraries. If BUILD_EXAMPLES is used by multiple libraries, then disabling it for one will disable it for all, requiring manual intervention. Unique variable names makes everyone's lives easier.

Now we can call FetchContent_MakeAvailable(), and import RayLib into our project:

FetchContent_MakeAvailable(raylib)

Linking to RayLib (and Other Libraries)

The previous section makes sure that the RayLib library is on our system. Now, it's time to link our code to it. This is a simple one-liner:

target_link_libraries(${PROJECT_NAME} raylib)

Add any other libraries that your program needs to the line above.

That's it. You can now build it using the usual method. For example, enter the following in a terminal window (from the same directory as the source-code):

mkdir build
cd build
cmake ..
cmake --build .

All going well, you should end up with a window saying "Hello world!"

hello world screenshot

Download the Full Example

Click here to download the full code.

Want to Master CMake? Get there Fast...

I'm working on a book/course that'll teach you the essentials of modern CMake by example. Save yourself hours of scouring the internet, and accidentally learning the old (messy) ways of doing things.

Click here to learn more

The CMake Tutorial Cover