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
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
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.

Screenshot of the github releases page

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/

Using Git

Those comfortable with git may want to retrieve the source code by cloning the repository into the desired directory, rather than downloading and extracting a zip file.

Note that the main branch is used for ongoing development of SDL3, which has not been released yet. As such, we should ensure our local working directory is using an SDL2 branch or release tag, such as release-2.30.x:

git clone -b release-2.30.x --depth 1 https://github.com/libsdl-org/SDL.git

If our main project is also a git repository, we may prefer to add SDL as a submodule instead, which can simplify our management further.

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;
}

Troubleshooting: Cannot open include file SDL.h

In some environments, we may need to qualify SDL.h with the SDL2/ directory when including it:

#include <SDL2/SDL.h>

Alternatively, we can add an additional target_include_directories() call to our CMakeLists.txt, referencing the /include directory within the SDL library code we downloaded:

target_include_directories(
  Sandbox PRIVATE
  external/SDL-release-2.30.2/include
)

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.

Why doesn’t SDL2main need to be copied?

SDLmain is an example of a static library. Static libraries get included in our executable file, so there is no external dependency.

We can identify a static library typically by the .lib extension on Windows, or the .a (abbreviation of archive) extension on Linux and macOS

Shipping our Program

When we share our program for others to use, we must remember that these dependencies exist.

Our program is no longer just an executable - we also need to provide all the additional dependencies (such as .dll files) so users can successfully run it.

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/

Using Git

As before, those comfortable with git may want to retrieve the source code by cloning the repository, rather than downloading and extracting a zip file.

Again we should ensure our working directory for each of these repositories is using a version 2 branch or release tag. For example:

git clone -b release-2.22.x --depth 1 https://github.com/libsdl-org/SDL_ttf.git
git clone -b release-2.8.x --depth 1 https://github.com/libsdl-org/SDL_image.git

If our main project is also a git repository, we may want to add these libraries as submodules, rather than standalone repositories.

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

PowerShell Execution Policies

Depending on your environment, attempting to run a PowerShell script may result in an error similar to the following:

Get-GitModules.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170.

This is a security feature, as running a PowerShell script from an untrusted source can compromise your security.

To allow PowerShell to run scripts, you will first need to run PowerShell as an administrator. This can be done by finding PowerShell in the start menu, right-clicking and selecting Run as Administrator

We can then remove the restriction using the following command:

Set-ExecutionPolicy -ExecutionPolicy Unrestricted

After running the Get-GitModules.ps1: script in the external directory, we can then reenable the restrictions as an administrator if preferred:

Set-ExecutionPolicy -ExecutionPolicy Default

If removing the restrictions is not an option, we can alternatively open a regular PowerShell window in the external directory, and copy and paste the following command: which will have the same effect as running the Get-GitModules.ps1 script:

foreach ($Line in Get-Content ..\.gitmodules) {
  [String] $PRegex = "path\s*=\s*(?<path>.*)"
  [String] $URLRegex = "url\s*=\s*(?<url>.*)" 
  [String] $BRegex = "branch\s*=\s*(?<Branch>.*)"
  if ($Line -match $PRegex) {
    $Match  = Select-String -InputObject $Line `
      -Pattern $PRegex
    $Path   = $Match.Matches[0].Groups[1].Value
  }
  elseif ($Line -match $URLRegex) {
    $Match  = Select-String -InputObject $Line `
      -Pattern $URLRegex
    $URL    = $Match.Matches[0].Groups[1].Value
  }
  elseif ($Line -match $BRegex) {
    $Match  = Select-String -InputObject $Line `
      -Pattern $BRegex
    $B = $Match.Matches[0].Groups[1].Value
     
    Write-Host `
      "git clone $URL $Path -b $B --recursive" `
      -ForegroundColor Blue
    git clone $URL ../$Path -b $B --recursive
  }
}

Using Git

The external dependencies are registered as git submodules, so if we cloned the repository, downloading the dependencies to the correct directory is slightly easier. We can simply use this command in the root of each of our repositories:

git submodule update --init --recursive

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:

#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_Window* Window{SDL_CreateWindow(
    "Hello Window", 0, 0, 800, 300, 0)};

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

  return 0;
}

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!

Was this lesson useful?

Next Lesson

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
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Setting up an SDL2 Development Environment
  • 60.GPUs and Rasterization
  • 61.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 62 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

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
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved