Building SDL2 from a Subdirectory (CMake)

A step-by-step guide on setting up SDL2 and useful extensions in a project that uses CMake as its build system

Ryan McCombe
Updated

In this lesson, we'll get SDL installed, along with two extensions which we'll need later in this chapter.

  • SDL is a cross-platform library that allows us to create windows, access input devices, and create graphics.
  • SDL_image is an extension that allows us to work with images in all the common formats, such as jpeg and png
  • SDL_ttf is an extension that we can use to render text at run-time

This setup guide is designed for projects that use the CMake build system, where the SDL libraries will be stored in a subdirectory of our project.

Downloading the SDL2 Source Code

The first step is to acquire the source code for SDL2. The latest version is available from the releases page on the official GitHub repository.

Important Note: this release page may include preview versions of SDL3. This course is using SDL2, so ensure the release you download starts with 2 (for example, 2.30.2)

At the bottom of the latest version 2 release, we should see links to download the source code, as either a .zip or a .tar.gz file.

Once downloaded, we should extract the files somewhere on our hard drive where we can easily access them.

Creating the Subdirectory

The next step is to extract this downloaded source code into a subdirectory of our project. For organizational reasons, third-party code that our project relies on is often stored in a directory called external or vendor.

We'll use external in this example, and by the end of the lesson our folder structure will look like this:

my-project/
 CMakeLists.txt
 main.cpp
 external/
   SDL-release-2.30.2/
   SDL-image-release-2.8.2/
   SDL_ttf-release-2.22.0/

Updating CMakeLists.txt

Let's now update our CMake project to make use of this new subdirectory. In this example, we have a minimalist project called Sandbox, which has a single main.cpp file:

cmake_minimum_required(VERSION 3.16)

set(CMAKE_CXX_STANDARD 20)

project(Sandbox VERSION 1.0.0)
add_executable(Sandbox main.cpp)

Note: In this example CMakeLists.txt file, we've called our executable Sandbox, but your name is likely to be different. Every time we use Sandbox in this lesson, you should replace it with the name you gave your project.

Using the add_subdirectory() function, we can tell CMake about our dependency:

add_subdirectory(external/SDL-release-2.30.2)

Because the SDL library also uses CMake, the add_subdirectory() command can find its CMakeLists.txt file, and understand the project. We simply need to declare that our executable has a dependency on SDL2.

target_link_libraries(Sandbox PRIVATE SDL2)

Simply using "SDL2" here works because SDL2 is the name of a project that is within SDL's CMakeLists.txt file, and CMake is aware of that file because of the add_subdirectory() command we used.

SDL2main

Note that if we're building our application for Windows, we should additionally add SDL2main as a dependency. CMake supports conditionals that can help us here:

if (WIN32)
  target_link_libraries(
    Sandbox PRIVATE SDL2main
  )
endif()

SDL needs to perform additional manipulation of our main function to make our program compatible with Windows, and this small additional library provides that functionality.

Our CMakeLists.txt file now looks like this:

cmake_minimum_required(VERSION 3.16)

set(CMAKE_CXX_STANDARD 20)

project(Sandbox VERSION 1.0.0)
add_executable(Sandbox main.cpp)

add_subdirectory(external/SDL-release-2.30.2)
target_link_libraries(Sandbox PRIVATE SDL2)

if (WIN32)
  target_link_libraries(
    Sandbox PRIVATE SDL2main
  )
endif()

Testing Compilation

With our project configured through CMake, we can now write a simple program to verify that it compiles:

#include <SDL.h>

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{SDL_CreateWindow(
    "Hello Window", 0, 0, 800, 300, 0)};

  SDL_Event event;
  while (true) {
    SDL_PollEvent(&event);
  }

  return 0;
}

Note that this program should compile at this point, but it may not run successfully. We may get an error similar to the following, which we'll address next:

The code execution cannot proceed because SDL2.dll was not found

Handling DLLs

Our executable file now has a dependency on the SDL dynamically linked library, which may have a name like SDL2.dll on Windows or SDL2.dylib on macOS and Linux.

When we run our executable, the operating system searches for this library, and may not be able to find it.

One of the locations it searches is the same directory as the executable file that requested the library, so this is a common place where we'd put our dependencies. We could move our library files into this location manually, but we'd typically prefer to automate this, and CMake can help us.

In most operating systems, we can copy a file from one location to another using a terminal command, like the following:

copy "folder-a/file.txt" "folder-b"

In our CMakeLists.txt, we can ask CMake to run this command after our Sandbox project builds:

add_custom_command(
  TARGET Sandbox POST_BUILD COMMAND
  copy "folder-a/file.txt" "folder-b"
)

CMake comes with a range of built-in scripts that are generally useful when working on build automation tasks. These are available on the command line using cmake -E, for example:

add_custom_command(
  TARGET Sandbox POST_BUILD COMMAND
  cmake -E copy_if_different
    "folder-a/file.txt"
    "folder-b"
)

There are two further benefits to using cmake -E over raw commands. First, the cmake -E scripts are platform agnostic. A command like copy will require a different syntax depending on whether the user is on Windows or Mac. But a command like cmake -E copy_if_different will work on any environment that has cmake.

Secondly, this approach also allows us to use generator expressions which can be used to dynamically generate information. We can identify generator expressions by the $<> syntax.

In this case, we can use generator expressions to figure out where the SDL library is located, and where it needs to be copied to.

  • $<TARGET_FILE:SDL2> generates the location of the main file produced by the SDL2 project defined within the CMakeLists.txt file of the subdirectory we added. That is, it generates the location of the .dll or .dylib file we need to copy.
  • $<TARGET_FILE_DIR:Sandbox> returns the directory where our Sandbox project will output its executable. That is, generates the location where we need to copy the .dll or .dylib file to
add_custom_command(
  TARGET Sandbox POST_BUILD COMMAND
  cmake -E copy_if_different
    $<TARGET_FILE:SDL2>
    $<TARGET_FILE_DIR:Sandbox>
)

As a final step, we can replace cmake with ${CMAKE_COMMAND}. CMAKE_COMMAND is a variable that stores the fully resolved path to the cmake executable. Telling the terminal exactly where cmake is located makes our command more resilient across different environments.

We also added the VERBATIM flag, asking the platform to interpret the command exactly as we've written it, which further improves resilience across different environments:

add_custom_command(
  TARGET Sandbox POST_BUILD COMMAND
  ${CMAKE_COMMAND} -E copy_if_different 
    $<TARGET_FILE:SDL2>
    $<TARGET_FILE_DIR:Sandbox>
  VERBATIM 
)

Our CMakeLists.txt file now looks like this:

cmake_minimum_required(VERSION 3.16)

set(CMAKE_CXX_STANDARD 20)

project(Sandbox VERSION 1.0.0)
add_executable(Sandbox main.cpp)

add_subdirectory(external/SDL-release-2.30.2)

target_link_libraries(Sandbox PRIVATE
  SDL2
)

if (WIN32)
  target_link_libraries(
    Sandbox PRIVATE SDL2main
  )
endif()

add_custom_command(
  TARGET Sandbox POST_BUILD COMMAND
  ${CMAKE_COMMAND} -E copy_if_different
    $<TARGET_FILE:SDL2>
    $<TARGET_FILE_DIR:Sandbox>
  VERBATIM
)

Additionally, after rebuilding our application, the SDL library should be copied into the correct location, allowing our program to run successfully.

Building SDL_image and SDL_ttf

SDL2 has a collection of official extensions, that can add further capabilities to our project. In this course, we'll use SDL_image for handling images, and SDL_ttf for rendering text.

To add them to our project, we first retrieve the source code for the latest 2.x releases. These are available from GitHub:

After downloading them, we can extract them into the same external directory we placed the base SDL2 library. After doing this, our directory structure might look something like this:

my-project/
 CMakeLists.txt
 main.cpp
 external/
   SDL-release-2.30.2/
   SDL-image-release-2.8.2/
   SDL_ttf-release-2.22.0/

Downloading Dependences

Unlike the main SDL2 library, the extensions like SDL_image and SDL_ttf have their own dependencies. These dependencies are not included in the main download - we have to acquire them separately, and place them in the external folder within each library:

my-project/
 CMakeLists.txt
 main.cpp
 external/
   SDL-release-2.30.2/
   SDL-image-release-2.8.2/
    external/ 
   SDL_ttf-release-2.22.0/
    external/

Within these directories, the SDL team has provided scripts that can automate this process for us.

macOS and Linux users can right-click the external folder, select Open Terminal Here, and then run the command:

./download.sh

Windows users can right-click the external folder, select Open in Terminal, and then run the command:

.\Get-GitModules.ps1

Adding Extensions to CMakeLists.txt

Once we've downloaded all the extensions we need into subdirectories, and provided all their dependencies, we can now add them to our project by updating our CMakeLists.txt.

We follow the same three steps we did when installing the initial SDL library:

  • We add the downloaded subdirectories using the add_subdirectory() function
  • We declare the libraries as dependencies of our executable by expanding the target_link_libraries() list
  • If we're using CMake to copy the libraries into the correct folder, we expand that command to also copy the SDL2_image and SDL2_ttf library files

Our complete CMakeLists.txt file might look something like this:

cmake_minimum_required(VERSION 3.16)

set(CMAKE_CXX_STANDARD 20)

project(Sandbox VERSION 1.0.0)
add_executable(Sandbox main.cpp)

add_subdirectory(external/SDL-release-2.30.2)
add_subdirectory(external/SDL_image-release-2.8.2) 
add_subdirectory(external/SDL_ttf-release-2.22.0) 

target_link_libraries(Sandbox PRIVATE
  SDL2
  SDL2_image 
  SDL2_ttf 
)

if (WIN32)
  target_link_libraries(
    Sandbox PRIVATE SDL2main
  )
endif()

add_custom_command(
  TARGET Sandbox POST_BUILD COMMAND
  ${CMAKE_COMMAND} -E copy_if_different
    "$<TARGET_FILE:SDL2>"
    "$<TARGET_FILE:SDL2_image>" 
    "$<TARGET_FILE:SDL2_ttf>" 
    "$<TARGET_FILE_DIR:Sandbox>"
  VERBATIM
)

We can validate everything is set up correctly by compiling and running the following program. We'll explain every line of this program soon but, for now, let's just confirm it compiles and runs:

#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  IMG_Init(IMG_INIT_PNG);
  TTF_Init();

  SDL_CreateWindow(
    "Hello Window",
    100, 100, 800, 300, 0
  );
  
  while(true) {
    SDL_PumpEvents();
  }
  
  return 0;
}

If everything is working correctly, we'll see a window pop up when our program runs:

Note that this example program does not allow itself to be closed. We'll add that capability later in the course. For now, we can close it using our IDE if possible, or force it to close through our operating system (Ctrl + Alt + Del on Windows or Cmd + Option + Esc on macOS).

We'll update our program to support quitting early in the next chapter.

Summary

In this lesson, we covered how to integrate the SDL2 library and its extensions into a C++ project using CMake.

By downloading the SDL source code into a subdirectory and updating our CMakeLists.txt file, we can build SDL alongside our project and easily manage dependencies. Key takeaways:

  • SDL2 source code should be downloaded and extracted into a subdirectory (e.g., external/) of our project
  • Extensions like SDL_image and SDL_ttf can be added in the same way, each in their own subdirectory
  • CMakeLists.txt must be updated to reference the SDL subdirectories with add_subdirectory() and link the libraries to our executable with target_link_libraries()
  • The compiled SDL dynamic libraries (DLLs) need to be accessible to our executable, which can be automated using CMake's add_custom_command() and generator expressions
  • When using git, SDL and its extensions can be added as submodules for easier management
  • On Windows, the additional SDL2main library must be linked to our executable for compatibility

With SDL2 set up in our project, we're ready to start developing our project!

Next Lesson
Lesson 16 of 129

Building SDL2 from Source (GCC and Make)

This guide walks you through the process of compiling SDL2, SDL_image, and SDL_ttf libraries from source

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Troubleshooting CMake Errors with SDL2
I followed the lesson but I am getting CMake errors when trying to build my project with SDL2. How can I troubleshoot this?
Using SDL2 without CMake
Is it possible to use SDL2 in my C++ project without using CMake? How would I set that up?
Enabling VSync with SDL2
How can I enable vertical synchronization (VSync) in my SDL2 application to prevent screen tearing?
Creating a Fullscreen Window with SDL2
How do I create a fullscreen window using SDL2 instead of a windowed one?
Playing Audio with SDL2
The lesson focused on graphics and input. How can I play audio in my SDL2 application?
Implementing a Basic Game Loop with SDL2
The lesson's example code uses a simple while loop for the main game loop. What does a more complete game loop look like in SDL2?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant