4
\$\begingroup\$

Introduction

This is my first CMake project. I had a bit of help with the initial setup but I have since made a lot of changes myself. It's working well but I think I am doing things in a slightly unconventional way.

In particular, I am unsure about:

1) Dependencies

Instead of using find_package (which I am still struggling to wrap my head around), I assume that all dependencies are placed, pre-built, into a libs folder.

I define a custom function for each one that can be used to add it to a project (e.g. CMake/AddFreetype.cmake, below). I also create user-facing properties so that the version number of each library can be specified.

One thing I like about this approach is that dependencies can be trivially shared between multiple projects. Adding a dependency requires only 1 line.

2) Auto-Generated Source Lists

For one of my projects, I have a script that outputs a CMake file containing all of its source and header files as CMake variables (${OPEN_RIVAL_SOURCES} and ${OPEN_RIVAL_HEADERS}). This has 2 advantages:

  1. I can programmatically regenerate the list of source/header files without needing to manually update CMake whenever I create or delete files.

  2. I can include the same set of files in my test project, without needing to duplicate the list of files.

To me these benefits are so obvious that I can't believe there isn't a more standardized way of achieving the same thing.

3) Precompiled Headers

I have included a handful of precompiled headers, but I am still not completely sure if this is beneficial or not. These headers are widely used throughout the project, but do the precompiled headers not get included in all header files, even those that don't need them? Could this actually be detrimental to the compilation process in some way?


Files

/CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(Open-Rival CXX)

include(CMake/AddFreetype.cmake)
include(CMake/AddGLEW.cmake)
include(CMake/AddGLM.cmake)
include(CMake/AddJson.cmake)
include(CMake/AddOpenAL.cmake)
include(CMake/AddRtMidi.cmake)
include(CMake/AddSDL2.cmake)
include(CMake/AddSDL2_Image.cmake)
include(CMake/AddSpdlog.cmake)
include(CMake/CompilerWarnings.cmake)
include(CMake/StandardProjectSettings.cmake)

set(CMAKE_CXX_STANDARD 20)

# Library directory
set(LIBS_DIR ${CMAKE_SOURCE_DIR}/libs CACHE STRING "Library directory")

# Dependency versions
set(FREETYPE_VERSION "2.13.1" CACHE STRING "FreeType version number")
set(GLEW_VERSION "2.1.0" CACHE STRING "glew version number")
set(GLM_VERSION "0.9.9.8" CACHE STRING "GLM version number")
set(JSON_VERSION "3.11.2" CACHE STRING "JSON version number")
set(OPENAL_VERSION "1.23.1" CACHE STRING "OpenAL version number")
set(RTMIDI_VERSION "5.0.0" CACHE STRING "RtMidi version number")
set(SDL2_VERSION "2.28.0" CACHE STRING "SDL2 version number")
set(SDL2_IMAGE_VERSION "2.6.3" CACHE STRING "SDL2_Image version number")
set(SPDLOG_VERSION "1.12.0" CACHE STRING "spdlog version number")

# This is seemingly an empty interface which all projects depend on... remove?
add_library(project_options INTERFACE)

# Option specifications
option(ENABLE_TESTING "Enable Test Builds" OFF)

# Projects can link with this "library" to use the warnings
# specified in CompilerWarnings.cmake
add_library(project_warnings INTERFACE)
set_project_warnings(project_warnings)

# Add projects
add_subdirectory(projects/Open-Rival)
add_subdirectory(projects/Open-Rival-test)
add_subdirectory(projects/setup)
add_subdirectory(projects/audio-extractor)
add_subdirectory(projects/campaign-extractor)
add_subdirectory(projects/image-extractor)
add_subdirectory(projects/interface-extractor)
add_subdirectory(projects/texture-builder)

# Set start-up project for Visual Studio
set(VS_STARTUP_PROJECT, Open-Rival)

CMake/AddFreetype.cmake

#
# Adds a dependency on Freetype to a project.
#
function(add_freetype project_name)

    # Detect 32- vs 64-bit systems
    if (CMAKE_SIZEOF_VOID_P EQUAL 8)
        set(_freetype_lib_suffix "objs/x64")
    else()
        set(_freetype_lib_suffix "objs/Win32")
    endif()

    # Include directory
    target_include_directories(${project_name} PRIVATE
        "${LIBS_DIR}/freetype-${FREETYPE_VERSION}/include"
    )

    # Library directory
    if(WIN32)
        target_link_directories(${project_name} PUBLIC
            "$<$<CONFIG:Debug>:${LIBS_DIR}/freetype-${FREETYPE_VERSION}/${_freetype_lib_suffix}/Debug>"
            "$<$<CONFIG:Release>:${LIBS_DIR}/freetype-${FREETYPE_VERSION}/${_freetype_lib_suffix}/Release>"
        )
    else()
        message(STATUS "Operating system not (yet) supported!")
    endif()

    # Library filename
    target_link_libraries(${project_name} PRIVATE
        freetype
    )

endfunction()

projects/Open-Rival/CMakeLists.txt

cmake_minimum_required (VERSION 3.16)

# Pull in auto-generated list of source/header files
set(OPEN_RIVAL_DIR ${CMAKE_CURRENT_LIST_DIR})
include(${CMAKE_SOURCE_DIR}/CMake/OpenRivalFiles.cmake)

# Not required for compilation, but helpful to have at hand
set(OPEN_RIVAL_DOCS
    ${CMAKE_SOURCE_DIR}/dist/docs/changelog.txt
    ${CMAKE_SOURCE_DIR}/docs/TODO.md
)

# Not required for compilation, but helpful to have at hand
set(OPEN_RIVAL_EXTRA_FILES
    ${CMAKE_SOURCE_DIR}/projects/.clang-format
)

set(OPEN_RIVAL_INCLUDE_DIRECTORIES
    ${CMAKE_CURRENT_LIST_DIR}/
    ${CMAKE_CURRENT_LIST_DIR}/include
)

set(OPEN_RIVAL_RESOURCES
    ${CMAKE_CURRENT_LIST_DIR}/Open-Rival.rc
    ${CMAKE_CURRENT_LIST_DIR}/resource.h
)

# Organise files into folders (filters)
source_group(TREE ${CMAKE_CURRENT_LIST_DIR}/src PREFIX "Source Files" FILES ${OPEN_RIVAL_SOURCES})
source_group(TREE ${CMAKE_CURRENT_LIST_DIR}/include PREFIX "Source Files" FILES ${OPEN_RIVAL_HEADERS})
source_group(TREE ${CMAKE_CURRENT_LIST_DIR} PREFIX "Resources" FILES ${OPEN_RIVAL_RESOURCES})
source_group("Docs" FILES ${OPEN_RIVAL_DOCS})

# Creates the executable including all the sources and resources
add_executable(Open-Rival
    ${OPEN_RIVAL_SOURCES}
    ${OPEN_RIVAL_HEADERS}
    ${OPEN_RIVAL_RESOURCES}
    ${OPEN_RIVAL_DOCS}
    ${OPEN_RIVAL_EXTRA_FILES}
)

# Add the directories and headers of the project
target_include_directories(Open-Rival PRIVATE
    ${OPEN_RIVAL_INCLUDE_DIRECTORIES}
)

# Dependencies
add_freetype(Open-Rival)
add_glew(Open-Rival)
add_glm(Open-Rival)
add_json(Open-Rival)
add_openal(Open-Rival)
add_rtmidi(Open-Rival)
add_sdl2(Open-Rival)
add_sdl2_image(Open-Rival)
add_spdlog(Open-Rival)

# Adds the libraries it must link against to find the symbols
target_link_libraries(Open-Rival PRIVATE
    winmm
    Ws2_32
    project_options
    project_warnings
)

# Pre-compiled headers
target_precompile_headers(Open-Rival PRIVATE
    <cstdint>
    <memory>
    <stdexcept>
    <string>
    <vector>
)

(Other files are not particularly important but can be found in the project repo.)

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I've been thinking about why this CMake script is awkward, I think I finally figured it out:

  1. It's specifically for Windows, the path organization, the way you name and link to libraries, would never work on other systems. You can't just fill in some code for the line message(STATUS "Operating system not (yet) supported!") and get it to make sense for macOS or Linux or Unix. [I haven't used Windows in 15 years or so, so I tend to find Windows things awkward...]

  2. It's a script that builds other projects. Instead of building each project independently, you build a script that builds all of your (unrelated?) projects. The only thing that these projects seem to have in common is that they link against the same set of libraries. And you can't build one of those projects without building all of them. By combining their library linking configurations in this way, you've made it impossible to separate out one of the projects for building elsewhere (i.e. distribute its source code so others can build it). [Maybe I misunderstand, and these are all tightly linked projects? If so, you might consider a different project organization.]

  3. You're linking the debug version of, say, FreeType when you're building the debug version of your project. If you're debugging your project, you're not debugging FreeType. Why would you ever build or link to a debug version of FreeType if you're not developing FreeType?

1) Dependencies

Usually you'd build and install these dependencies (or install them though a package manager). You then link to the installed libraries, not the copy in their build directory. This is where find_package comes in. CMake comes with scripts to find and configure many common libraries (see the Find*.cmake scripts here). And packages build though CMake typically install a custom CMake script that find_package will find and use to configure the library settings.

The advantage to using find_package is that a libraries dependencies, compile-time requirements, etc are all taken care of. All you need to do is target_link_libraries(<target>, <library>), and all required include directories, compiler flags, linker flags etc are automatically included when you build <target>.

But yes, it does require that you either install the library you want to link to, or that you explicitly give its path if it's in a non-standard location.

For example:

set(FREETYPE_DIR "/path/to/freetype/lib")
find_package(Freetype REQUIRED)
target_link_libraries(Open-Rival, Freetype::Freetype)

2) Auto-Generated Source Lists

First of all, you don't need to add include files to the list of project files. Only the actual translation units must be listed. Documentation files should certainly not be listed as project sources.

If you don't want to hard-code the list of source files in your CMake script, you could use file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp"), but it is recommended to explicitly list source files. CONFIGURE_DEPENDS can fail, according to the CMake documentation.

I found this solution to be a good compromise: running CMake will generate a list of project files and save it to a file, that you'd commit into your repository. When someone else pulls updates and rebuilds, they don't run the risk of not building the new file you added, because the file containing the list of project files changed for them as well, even if they didn't re-run CMake. This is the CMake function I adapted from that answer.

Of course, manually maintaining a lists of project files is not that hard, and is likely the simplest and most useful approach

I can include the same set of files in my test project, without needing to duplicate the list of files.

Your test project should probably be a sub-project of, or a second target in the same project as, the project you are testing. You don't need the same list of source files in two projects, each set of source files should constitute a single project!

3) Precompiled Headers

I don't know if it makes a difference or not to include these precompiled headers. But it is simple for you to find out: build the project with this target_precompile_headers() command in it, then comment it out and re-build. Time both builds, and decide whether to keep it in or not.

\$\endgroup\$
8
  • \$\begingroup\$ Thank you for the detailed review, I will certainly take your comments onboard. Is there any way I can respond in-depth? \$\endgroup\$
    – Dan
    Jul 28, 2023 at 8:00
  • \$\begingroup\$ I will list the main points here for now. (1) Yes, it is all written with Windows in mind for now. Perhaps it would need reworking for other OSes but that is a very low priority for me right now. (2) The reason for using a single CMake build for all of my projects is that I want to keep them all in the same solution. They are all related, even if they can be compiled and run independently of each other. (3) I thought this was common practice. If a library throws an error, doesn't using the debug version allow me to see where the error originated? \$\endgroup\$
    – Dan
    Jul 28, 2023 at 8:07
  • \$\begingroup\$ Dependencies: I have tried providing the path explicitly but I have struggled with this in the past. Every library seems to follow a different convention and I've found it difficult to find clear documentation. Many libraries do not have an official Find* script, and there are dozens of variants online with subtle differences between them. Nonetheless, I will give this another go, because it sounds like a cleaner and more portable approach. \$\endgroup\$
    – Dan
    Jul 28, 2023 at 8:19
  • \$\begingroup\$ "Documentation files should certainly not be listed as project sources." I find it helpful to have these files contained within the project for easy access inside my IDE. Is there a better way to include them? // "You don't need the same list of source files in two projects, each set of source files should constitute a single project!" The set of source files is not exactly the same; the test project includes most of the same sources, but removes some and adds some others. \$\endgroup\$
    – Dan
    Jul 28, 2023 at 8:28
  • \$\begingroup\$ @Dan Stack Exchange doesn’t really allow for a back-and-forth, unfortunately. Debug libraries: most likely the error happens in your own code, knowing what happens inside the library doesn’t help you fix your own error. Of course it’s possible for the library to have a bug, then you can to set out to debug the library if you like. But large, well-used libraries are much less likely to have a bug that your own code. \$\endgroup\$ Jul 28, 2023 at 14:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.