2

My goal is to build a C++ app with SDL2 that can run on both Linux and Windows which may not have SDL2 installed. (I know that there are lot of posts about it, I'll come later).

So I'm going through my little project structure and I remember using CMake to make building more efficient. So I document myself a lot and conclude to this structure:

- app (contains the main function)
  |- main.cpp
  |- CMakeLists.txt
- build
- libs (contains externals libraries such as: "spdlog", "googletest")
- src
  |- all the source files
  |- CMakeLists.txt
- tests
  |- tests files
  |- CMakeLists.txt
CMakeLists.txt

Structure is not the point of this discussion.

Now that my project structure is done, I focus on my build files.

Thanks to a lot of posts, I understand that I should go through static linking. (I know the cons such as recompiling statics libraries, not having last updates automatically, larger files, ...). I want to distribute my program as a "folder" so I excluded the solution of (package) installers. My program must be able to run with his own resources (folders). Also, remember that SDL2 came with the "zlib license" allowing static link.

So I write my src/CMakeLists.txt:

set(PROJ_LIB_NAME "projectlib")

configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/config/cmake_config.hpp.in 
    ${CMAKE_CURRENT_SOURCE_DIR}/config/cmake_config.hpp
)

set(PROJ_LIB_SOURCES
    core.cpp
    utils/logs_utils.cpp
    gui/gui.cpp
)

add_library(${PROJ_LIB_NAME} STATIC ${PROJ_LIB_SOURCES})
target_include_directories(${PROJ_LIB_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

### ====================================
### LIBRARIES
### ====================================

### spdlog
### ====================================
if(NOT TARGET spdlog)
    # Stand-alone build
    find_package(spdlog REQUIRED)
endif()
target_link_libraries(${PROJ_LIB_NAME} PRIVATE spdlog::spdlog)


### SDL2
### ====================================
find_package(SDL2 REQUIRED)
target_include_directories(${PROJ_LIB_NAME} PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(${PROJ_LIB_NAME} PRIVATE -static ${SDL2_LIBRARIES})

Please note the -static at the last line

And my CMakeLists.txt (root):

cmake_minimum_required(VERSION 3.16.3 FATAL_ERROR)
enable_testing()

set(PROJ_NAME "progr")
set(PROJ_VERSION "1.0.0")

# set the project name
project(${PROJ_NAME} VERSION ${PROJ_VERSION})

# specify the C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# specify compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wpedantic")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")

add_subdirectory(${CMAKE_SOURCE_DIR}/libs/googletest-1.11.0)
add_subdirectory(${CMAKE_SOURCE_DIR}/libs/spdlog-1.8.5)
add_subdirectory(${CMAKE_SOURCE_DIR}/src)
# add_subdirectory(${CMAKE_SOURCE_DIR}/tests)
# add_subdirectory(${CMAKE_SOURCE_DIR}/app)

Finally, the app/CMakeLists.txt:

set(PROJ_EXE_NAME "main")

add_executable(${PROJ_EXE_NAME} main.cpp)
target_link_libraries(${PROJ_EXE_NAME} PUBLIC projectlib)

Seems good but doesn't work. If I uncomment add_subdirectory(${CMAKE_SOURCE_DIR}/app) to build my app, I get a bunch of undefined references coming from "/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a", " ../libs/spdlog-1.8.5/libspdlogd.a", "/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/libSDL2.a"...


What I've tried so far to make my app standalone portable:

  • downloading the SDL2 source code and put it in libs/, telling CMake to treat it like a subdirectory -> that was dumb because the source code is aiming to install SDL on your computer, not to be used as it by your project... (following SDL installation guide, you can)
  • looking for a findSDL2.cmake file -> do I really need of those incomprehensible file? Also, it was good for SDL1 but not anymore for SDL2...
  • playing with so much differents parameters in CMake since 2 days...
  • many more that I can't remember for now...

There is not a single solution that everyone applauds.

I don't understand why there are no tutorials on how to achieve the goal of making a C++ SDL2 portable app even on machines where SDL2 is not installed... Everyone seems to agree that apps like Steam will definitely be installed and that it is up to them to manage SDL. What about games that don't go through Steam then? But then again I wonder if this is a good idea. As a JS developer, when I build an app with NPM dependencies, I block my packages versions so that all developers are working with the same tools and my production environment is stable. I don't understand this dynamic library logic so I'll be very happy to have explanations :)


EDIT: Forgot to mention that my project library (src/CMakeLists.txt) is building well and all goes wrong when I try to link it with my main fucntion (app/CMakeLists.txt)


EDIT 2: Have a working static link but with some others dependencies that must be downloaded... What I've done so far:

  • downloaded the SDL source code and put it in my /libs folder (https://www.libsdl.org/download-2.0.php) (No need to create a build directory inside and run 'make' or 'configure' commands. CMakeLists will handle this for you. Also, do not 'make install')
  • modify my root CMakeLists.txt to add add_subdirectory(${CMAKE_SOURCE_DIR}/libs/SDL2-2.0.14) before building my sources
  • modify my src/CMakeLists.txt to comment all the SDL find_package things (etc...) and add only the libraries linking. See:
### SDL2
### ====================================
## Comments
#set(SDL2_DIR ${CMAKE_SOURCE_DIR}/libs/SDL2-2.0.14)
#find_package(SDL2 REQUIRED)
#target_include_directories(${PROJ_LIB_NAME} PRIVATE ${SDL2_INCLUDE_DIRS})
#target_link_libraries(${PROJ_LIB_NAME} PRIVATE -static ${SDL2_LIBRARIES})
## end Comments

## the only needed link to SDL2
target_link_libraries(${PROJ_LIB_NAME} PRIVATE SDL2main SDL2-static)
  • after that, running my app give me some errors as "No available video device" at the initialization of SDL but my app was building!
  • turns out that I need some more dependencies which are installed (I guess) when you do sudo apt install libsdl2-dev (what we want to avoid). You can found all the dependencies here: https://github.com/libsdl-org/SDL/blob/main/docs/README-linux.md#build-dependencies
  • I've installed the dependencies and... Everything works fine!

I guess my new problem now is "What if I try to run my app on a computer that does not have these dependencies?". Asking that because I manage to compile and run my app before installing them. I conclude that my executable has the SDL code in it but not the dependencies code.

I still wonder why the static link is always so demonizing when for me it allows to have control over the versions of the dependencies. I agree with the "bug fixes" argument, but these new versions may be incompatible with your app. In this case, you should explain to your users that you are working very hard to release a fix when your code is not causing the regression...

genpfault
  • 51,148
  • 11
  • 85
  • 139
spMaax
  • 365
  • 2
  • 11

3 Answers3

2

Answering the question in the title, ignoring the error in the question. I'm going to suggest a different solution.


On Windows (assuming MinGW) this is easy. Static linking doesn't really matter: if you don't do it, you just ship all necessary dlls with your executable.

The list of required dlls is determined as follows:

  • Make sure the dlls shipped with your MinGW and libraries don't overlap with the ones in C:\Windows (including subdirectories). If you see an overlap, delete the matching dlls in C:\Windows. Some crappy installers like to put custom dlls in there, which tend to cause problems.

  • Open a shell, and in it prepend MinGW's bin/ directory to the PATH.

  • Use ntldd -R <filename.exe> to get a list of dlls. The ones in MinGW's bin/ you have to ship.


On Linux, there are several prominent solutions: Appimage, Flatpak, Snap.

I don't have too much experience with this, but here's what I've been doing:

  • Make sure you're running the oldest Linux version you wish to support (I used Ubuntu 18.04). You can run it under Docker.
  • Build SDL2 from sources, don't get it from a package manager. At least on Ubuntu, I heard that the packaged version doesn't dynamically look up available dependencies at runtime, decreasing portability.
  • Use ldd to determine the list of dependencies. Which ones need to be shipped has to be determined experimentally. Start by copying all of them to the current directory.
  • Use patchelf --set-rpath '$ORIGIN' <filename> on all those libraries and on your executable. (The 's are important, you don't want $ORIGIN to be expanded as a shell variable.)
  • Copy the whole directory to a different system (or a bunch of them), and see if it runs. You'll have to remove some of the shared libraries you copied, until it starts working. I ended up with just libstdc++.so.6, libgcc_s.so.1, and the libraries I used.

Alternatively, you can use a launcher script that sets LD_LIBRARY_PATH instead of patchelf.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Thanks for your answer and for all the tips! Very useful to me :) It's true that providing the .dll for the Windows version is not that restrictive. And for Linux, these tools seem to me to be a good solution but the best solution must be the static link no? I mean, it was design for this specific usage no? – spMaax Jun 28 '21 at 17:49
  • @spMaax I don't know, but I keep hearing that "static linking is broken on Linux", and so on. So I'm not sure. – HolyBlackCat Jun 28 '21 at 18:21
  • @HolyBlackCat Static linking is **NOT** broken on Linux. In fact, I just did it now! – ThatBirdThatLearnedToCode Feb 13 '22 at 21:57
  • @ThatBirdThatLearnedToCode [Here's what I'm talking about](https://stackoverflow.com/q/3430400/2752075). – HolyBlackCat Feb 13 '22 at 22:03
  • @HolyBlackCat Oh intresting... I didn't use the -static flag, and I use g++ too! – ThatBirdThatLearnedToCode Feb 14 '22 at 13:39
  • @ThatBirdThatLearnedToCode How did you link statically without `--static`? – HolyBlackCat Feb 14 '22 at 17:29
  • @HolyBlackCat The steps are the following: 1. Compile all c++ files (with -c flag, and don't do *.cpp, each one at a time) 2. Use `ar rs` to create an archive (ex: `ar rs libtest.a file1.o file2.o` and it should start with lib and end with .a) 3. Link it with the following added to your c++ compile command `-L. lib test.a` `L.` says look in the current dir for libraries. You can change that.) To automate this, I wrote a bash file. If you need that, I can send you it. (The steps were created with a mix of tutorials) Best to put this in your thing, as this is helpful. – ThatBirdThatLearnedToCode Feb 14 '22 at 20:48
  • @ThatBirdThatLearnedToCode Static linking a single library is simple, especially if you made the library yourself. The hard part is static linking every single third-party lib your code uses. – HolyBlackCat Feb 14 '22 at 20:55
  • Let's [continue this in chat](https://chat.stackoverflow.com/rooms/242013/discussion-between-thatbirdthatlearnedtocode-and-holyblackcat). – ThatBirdThatLearnedToCode Feb 14 '22 at 20:57
1

All of this assumes a recent version of CMake - you may need to install a newer version if this does not work for you.

So, first, you want to compile SDL2. On Linux, for the latest version, this would mean involve downloading and building the source code like this:

wget https://www.libsdl.org/release/SDL2-2.0.14.tar.gz
tar -xvf SDL2-2.0.14.tar.gz
cd SDL2-2.0.14
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=~/SDL2
make -j8
make install

This means you now have a built copy of SDL2 on your machine in the ~/SDL2 folder (you probably want to change this location). CMake has a built in FindSDL module which is quite old fashioned, but to use it add the following to your CMakeLists.txt

find_package(SDL 2 REQUIRED)
# When building an application:
include_directories(${SDL_INCLUDE})
target_link_libraries(myapplication SDL::SDL)

Then, when you call at the command line to build your project, do:

cmake .. -DSDL_LIBRARY=~/SDL2/lib -DSDL_INCLUDE_DIR=~/SDL2/include

For Windows, it's much the same procedure with CMake, except that if you are using VS2019, you use CMake through the GUI and so set parameters within that rather than on the command line. You can do it on the command line if you open a developer command prompt from the start menu though (this is needed so that the compiler variables are set for CMake to find).

In terms of other libraries - on Linux you will always need to depend on the C standard library (libc) supplied by the system (unless you use musl). Generally, this is forwards compatible but not backwards compatible, so many developers compile their code on a very old OS in order to ensure it runs 'everywhere'. You'll also need to statically link in libstdc++. On Windows, redistributable packages exist (for e.g. "Visual C++ Redistributable for Visual Studio 2019") which are generally distributed with your application or assumed to be installed.

  • Thank you very much for taking the time to answer my problem but I think we did not understand each other ... I manage to compile my project (my library in reality) with SDL2. For that, I went through the Ubuntu package manager. I ran the libsdl2-dev command. What I'm looking to do now is make sure that when my app is distributed, my non-SDL users won't have to install it before launching my app (on both Linux and WIndows). So I want to release my app with SDL2 in it. But still thanks for your answer, I will look to link libstdc++ statically too. – spMaax Jun 28 '21 at 16:59
  • 1
    I completely understand that. Ubuntu does not supply a static copy of SDL2 in the `libsdl2-dev` package (you can check this by running `pkg-config sdl2 --static`) but only the shared library, so you need to build it yourself if you want your app to be run anywhere. – Ryan Pepper Jun 28 '21 at 17:07
  • Oh ok, I didn't know. I'll try your solution but still good to know that my package doesn't contain the static version! – spMaax Jun 28 '21 at 17:26
0

You can use the method that I used for my project, I use SDL2 and SLD_Image and get them via FetchContent to a particular TAG on their repo.

cmake_minimum_required(VERSION 3.24)
project(sdl_test)

include(FetchContent)
Set(FETCHCONTENT_QUIET FALSE)

FetchContent_Declare(
        SDL2
        GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
        GIT_TAG release-2.26.3
        GIT_SHALLOW TRUE
        GIT_PROGRESS TRUE
)
FetchContent_MakeAvailable(SDL2)

FetchContent_Declare(
        SDL2_image
        GIT_REPOSITORY https://github.com/libsdl-org/SDL_image.git
        GIT_TAG release-2.6.3
        GIT_SHALLOW TRUE
        GIT_PROGRESS TRUE
)

set(SDL2IMAGE_INSTALL OFF)
set(BUILD_SHARED_LIBS FALSE)

FetchContent_MakeAvailable(SDL2_image)

add_executable(sdl_test main.cpp)
target_link_libraries(sdl_test SDL2::SDL2main SDL2::SDL2-static SDL2_image::SDL2_image-static)

With this you will have it statically and works cross-platform.

Include directories will work as usual.

#include <SDL.h>

remember to have your main in this format

int main(int, char *[]) {
....
}

Juan Medina
  • 533
  • 1
  • 5
  • 12