Introduction
For those of you that don’t know CMake is a cross platform build system that allows you to define what you want to build and how, before it goes off and creates the correct build scripts for your particular platform and compiler. It is supposed to help you to simplify your build process when compiling for multiple platforms by just having to maintain a single build script that can generate all other build scripts.
Generally I never had a use for a such a thing since I did all my work on Windows with Visual Studio however when I started doing work I eventually wanted to use on Linux too, I knew I had to look into something. OGRE had started using CMake to generate its build scripts, so I thought I’d look into that. This turned out to be much harder than I expected considering the documentation for CMake seems to range from poor to non-existent and that the examples they link to are either really quick overviews, or really contrived and complicated.
Still I persisted, and with the use of the OGRE CMake files as an example, and with the help of jacmoe and CABAListic I managed to get a CMake file that worked. It was ugly, it was all in one CMakeLists.txt file (which you shouldn’t really do) but at least it managed build two of my libraries on Linux. Of course, I was never able to test if these libraries ran on Linux since I never worked out how to link those libraries against an executable (or even how to recurse into my examples directory so I could attempt to build the examples). After 6 months I figured it was time for me to work out how to use CMake “properly”, I made myself a simple test case that represented what I ultimately needed to do, and then tried to build the CMake script to generate the correct build scripts for it.
Test Case
For my test case, I have two projects; one that should build as a static library, and one that should build as an executable linked against the static library. These projects are called “Core” and “Other” respectively and are arranged in a directory structure as shown below.
/ - The root SDK level
/Bin - Folder to place the built executables in
/Lib - Folder to place the built libraries in
/Source - Contains the source code for the projects
/Source/Core - Contains the source code for the Core project
/Source/Other - Contains the source code for the Other project
Now to use CMake properly, we are going to need four CMakeLists.txt files, three of which will contain actual project information (the SDK level, Core level and Other level ones), and one of which will just include some subdirectories (the Source level one). Now let’s take a look at the contents of each of those CMakeLists.txt files in turn.
SDK Level CMakeLists.txt
The root SDK level CMakeLists.txt contains the most code in it since it sets up the environment for the rest of the project, as well as having some functions that are used in the other CMakeLists.txt files to make adding more projects easier and reducing code duplication.
project(SDK)
cmake_minimum_required(VERSION 2.6)
set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS TRUE)
message(STATUS "Processing ${PROJECT_NAME}")
This first section sets up some basic information. It tells CMake that we are building a project called “SDK”, let’s it know the minimum CMake version this file will work with, and also just displays an informative message when CMake generates this project so we know what’s going on. PROJECT_NAME is a CMake variable that will be set to the name of the current project, there are more of these useful variables and they are listed on this wiki page. The ${ and } around PROJECT_NAME are used to tell CMake that we want to use the actual variable, and not just the string “PROJECT_NAME”.
The next section defines some helper functions that are used through the CMakeLists.txt files of the other projects to help remove code duplication.
# Function to list all header files in the current directory, recursing into sub-directories
# HEADER_FILES - To be filled with the found header files
function(sdk_list_header_files HEADER_FILES)
file(GLOB_RECURSE HEADER_FILES_TMP "*.h" "*.hpp" "*.inl" "*.pch")
set(HEADER_FILES ${HEADER_FILES_TMP} PARENT_SCOPE)
endfunction()
# Function to list all source files in the current directory, recursing into sub-directories
# SOURCE_FILES - To be filled with the found source files
function(sdk_list_source_files HEADER_FILES)
file(GLOB_RECURSE SOURCE_FILES_TMP "*.c" "*.cpp")
set(SOURCE_FILES ${SOURCE_FILES_TMP} PARENT_SCOPE)
endfunction()
# Function to setup some standard project items
# PROJECTNAME - The name of the project being setup
# TARGETDIR - The target directory for output files (relative to CMAKE_SOURCE_DIR)
function(sdk_setup_project_common PROJECTNAME TARGETDIR)
# Set the Debug and Release names
set_target_properties(
${PROJECTNAME}
PROPERTIES
DEBUG_OUTPUT_NAME ${PROJECTNAME}_d
RELEASE_OUTPUT_NAME ${PROJECTNAME}
)
# Add a post-build step for MSVC to copy the output to the target directory
if(MSVC)
add_custom_command(
TARGET ${PROJECTNAME}
POST_BUILD COMMAND
copy \"$(TargetPath)\" \"${CMAKE_SOURCE_DIR}/${TARGETDIR}\"
)#"
endif()
# Setup install to copy the built output to the target directory
# (for compilers that don't have post build steps)
install(
TARGETS ${PROJECTNAME}
LIBRARY DESTINATION "${CMAKE_SOURCE_DIR}/${TARGETDIR}"
ARCHIVE DESTINATION "${CMAKE_SOURCE_DIR}/${TARGETDIR}"
RUNTIME DESTINATION "${CMAKE_SOURCE_DIR}/${TARGETDIR}"
)
endfunction()
# Function to setup some project items for an executable or DLL
# PROJECTNAME - The name of the project being setup
function(sdk_setup_project_bin PROJECTNAME)
sdk_setup_project_common(${PROJECTNAME} Bin)
endfunction()
# Function to setup some project items for static library
# PROJECTNAME - The name of the project being setup
function(sdk_setup_project_lib PROJECTNAME)
sdk_setup_project_common(${PROJECTNAME} Lib)
endfunction()
That’s quite a lot of code, so I’ll quickly run through what it all does.
The sdk_list_header_files and sdk_list_source_files use something that CMake calls “file globbing” to check each file in the current directory against a glob expression and if they match, adds them to a list; in this case, I am using it to find all files with a certain file extension.
The sdk_setup_project_common, sdk_setup_project_bin and sdk_setup_project_lib functions all work together to setup some common target information for a project. You should recall from my test case explanation that I wanted built executables to go to the Bin directory, and built libraries to go to the Lib directory; well these are the functions that deal with that. sdk_setup_project_common does all the hard work, specify a post-build and install step to copy the built objects to the target directory, with sdk_setup_project_bin and sdk_setup_project_lib just providing the correct target directory. Since both the debug and release builds are going to the same directory, we need a way to distinguish between the two so they don’t overwrite each other; this is handled by set_target_properties which adds an “_d” to the object name when building in debug.
The next section sets up the basic build environment. I copied this section straight from the OGRE CMake scripts and it sets a load of flags on your compiler based on your platform, compiler, and architecture and can likely be left alone for most cases. That said, I won’t be going over it here.
The last section of this file is really simple.
include_directories("${PROJECT_SOURCE_DIR}/Source")
add_subdirectory(Source)
include_directories is used to add an include path to your compiler so it knows where to find header files; in my case, I just need the top level Source directory so that’s all I add. You will also need to use this to add any third-party include paths so your compiler can find it. add_subdirectory causes CMake to recurse into the subdirectory specified and nicely ends the main SDK CMakeLists.txt file.
Source Level CMakeLists.txt
This is a super simple file, all it does is tell CMake to recurse into the two project directories.
add_subdirectory(Core)
add_subdirectory(Other)
Core Level CMakeLists.txt
I will skip over the top two lines since we have seen those before in the SDK file. Basically we are just saying that this file will create a project called “Core”.
All our work in creating those functions earlier makes adding all the header and source files really simple.
sdk_list_header_files(HEADER_FILES)
sdk_list_source_files(SOURCE_FILES)
If you don’t want to add all the files from a certain directory, you can either just setup those variables using a manual list, or if it is easier, let the file globbing do its work and then remove the files you don’t want from the list.
add_library(${PROJECT_NAME} STATIC ${HEADER_FILES} ${SOURCE_FILES})
sdk_setup_project_lib(${PROJECT_NAME})
The last part of the file sets up this project to build as a static library, and uses our sdk_setup_project_lib to configure everything else so it gets copied to the correct location after being built. It should be noted this it is important that you call add_library before calling sdk_setup_project_lib otherwise sdk_setup_project_lib won’t know which target to modify since it won’t be able to find it.
Other Level CMakeLists.txt
Once again, I will skip over most of this file as it is all stuff you have seen before in the Core CMakeLists.txt file. The only part that is different is the end, so that is what I will focus on.
add_executable(${PROJECT_NAME} ${HEADER_FILES} ${SOURCE_FILES})
sdk_setup_project_bin(${PROJECT_NAME})
add_dependencies(${PROJECT_NAME} Core)
target_link_libraries(${PROJECT_NAME} Core)
Unlike the Core, Other has to build as an executable so we use add_executable and sdk_setup_project_bin instead of add_library and sdk_setup_project_lib to handle this.
Also unlike Core, Other has a dependency. add_dependencies is used to inform your compiler that Other depends on Core, and that Core must have been built before Other. target_link_libraries is used to tell the compiler that Other must link against the Core library.
Conclusion
Overall I feel CMake is a useful tool for when you need to create build scripts for multiple platforms, however I feel that the project currently suffers from poor documentation and examples. Finding out how to do anything often leaves me feeling like Batman trying to solve Edward Nigma’s riddles. Still, I managed to get there in the end.
comments powered by Disqus