In the previous post, I showed demos of CMake scripts that I am using in one of my projects and gave descriptions to what each function was doing. Though this is nice, sometimes when you are in the development process, you need more functionality from your build system. Below are just a few tricks I’ve learned using CMake that might save you a whole lot of time (But also might sacrifice some of CMake’s functionality).
Custom Configurations
Sometimes in projects, you want to have custom configurations that define different things. This is nice because you wont have to reconfigure if you want to run your code in a different way – though rebuilding is always needed when adding/removing definitions. Configurations can be changed by setting them via the CMAKE_BUILD_TYPE variable for makefiles, or by the Build Configuration drop-down menu in Visual Studio.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
############################################################################### # add_configuration(ConfigurationName [DEBUG] [BaseConfiguration]) ############################################################################### # Given a BaseConfiguration to copy from, add_configuration will create new # CMake Cache variables and configuration information for a new configuration. # The configuration can be modified post add_configuration call. # # If no BaseConfiguration is provided, CMake will initialize the cache # variables to empty strings (By attempting to read invalid strings). # # DEBUG can be provided to say that the configuration must link with debug # runtimes. This is also an optional argument. ############################################################################### function(add_configuration ConfigurationName) # Make sure C/CXX are enabled before doing this enable_language(C CXX) # Find our optional flags if("${ARGV1}" STREQUAL "DEBUG") set(DEBUG_CONFIGURATION TRUE) set(BaseConfiguration "${ARGV2}") elseif("${ARGV2}" STREQUAL "DEBUG") set(DEBUG_CONFIGURATION TRUE) set(BaseConfiguration "${ARGV1}") else() set(BaseConfiguration "${ARGV1}") endif() # Create capital versions string(TOUPPER "${ConfigurationName}" CONFIGURATIONNAME) string(TOUPPER "${BaseConfiguration}" BASECONFIGURATION) # Check other potential errors if(${ARGC} GREATER 3) message(FATAL_ERROR "Malformed configuration detected! Expected max 3 arguments but provided ${ARGC}!") endif() if(BaseConfiguration AND NOT DEFINED CMAKE_EXE_LINKER_FLAGS_${BASECONFIGURATION}) message(FATAL_ERROR "Invalid base configuration supplied to add_configuration: ${BaseConfiguration}") endif() # Create the configuration list(APPEND CMAKE_CONFIGURATION_TYPES ${ConfigurationName}) set(CMAKE_C_FLAGS_${CONFIGURATIONNAME} "${CMAKE_C_FLAGS_${BASECONFIGURATION}}" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS_${CONFIGURATIONNAME} "${CMAKE_CXX_FLAGS_${BASECONFIGURATION}}" CACHE STRING "" FORCE) set(CMAKE_EXE_LINKER_FLAGS_${CONFIGURATIONNAME} "${CMAKE_EXE_LINKER_FLAGS_${BASECONFIGURATION}}" CACHE STRING "" FORCE) set(CMAKE_SHARED_LINKER_FLAGS_${CONFIGURATIONNAME} "${CMAKE_SHARED_LINKER_FLAGS_${BASECONFIGURATION}}" CACHE STRING "" FORCE) set(CMAKE_MODULE_LINKER_FLAGS_${CONFIGURATIONNAME} "${CMAKE_MODULE_LINKER_FLAGS_${BASECONFIGURATION}}" CACHE STRING "" FORCE) # Register with CMake list(APPEND CMAKE_CONFIGURATION_TYPES ${ConfigurationName}) list(REMOVE_DUPLICATES CMAKE_CONFIGURATION_TYPES) set(CMAKE_CONFIGURATION_TYPES "${CMAKE_CONFIGURATION_TYPES}" CACHE STRING "" FORCE) # Done, print the status if(DEBUG_CONFIGURATION) get_property(DEBUG_CONFIGURATIONS GLOBAL PROPERTY DEBUG_CONFIGURATIONS) list(APPEND DEBUG_CONFIGURATIONS ${ConfigurationName}) set_property(GLOBAL PROPERTY ${CONFIGURATION} ${DEBUG_CONFIGURATIONS}) message(STATUS "Added ${ConfigurationName} debug configuration") else() message(STATUS "Added ${ConfigurationName} configuration") endif() endfunction() # Note: We must be sure to initialize valid configurations to the CMake defaults set(CMAKE_CONFIGURATION_TYPES "Debug;MinSizeRel;Release;ReleaseWithDebInfo") |
The function above works by first enabling C/C++ features so that the cache variables are initialized, and then by going on to create all the custom variables you need and registering everything properly with CMake. It also does some light error checking, such as proper argument count, and valid base configurations. This allows the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Instantiate our function include(AddConfiguration.cmake) # Add new configurations add_configuration(Publish Release) add_configuration(Development DEBUG Debug) add_configuration(Blank) add_configuration(BlankDebug DEBUG) # Edit the new configurations just like you edit existing configurations set(CMAKE_CXX_FLAGS_PUBLISH "${CMAKE_CXX_FLAGS_PUBLISH} -/DPUBLISH_MODE" CACHE STRING "" FORCE) # IMPORTANT! Instantiate the project AFTER adding configurations. project(my-project CXX) |
This can be very helpful for adding configurations that you may want to switch to during development. I wouldn’t recommend creating many of these, it’s really just for defining certain modes that you want to be able to switch back-and-forth from quickly. An example of this is a LeakDetect mode which will build with leak detecting tools. An example of when this is not needed is when you will spend lots of time in a specific custom configuration. It might have just been better to make it a configuration option at that point.
Setting Folder Properties
If you are using an IDE (such as Visual Studio), you will probably get some mileage out of your build targets being put into subfolders (highly recommended). Luckily, it’s not as involved as the command above.
1 2 3 4 5 |
# Separate Projects into folders for IDEs. set_property(GLOBAL PROPERTY USE_FOLDERS ON) # Put target named "my-target" into a folder named "my-folder" set_property(TARGET my-target PROPERTY FOLDER "my-folder") |
It’s as easy as that! You can get into much more depth with IDE customization, such as filter processing and other IDE-specific things. But this is an absolute must if you have developers who are using Visual Studio. It’s a tiny bit of work that will clean up so much of the solution file.
File Globbing
One of the things the creators of CMake don’t want you to do (and for good reason) is file globbing to form targets. However, on a project that is in rapid development, file globbing can save a lot of time. This is essentially having CMake find all files in a directory instead of you listing the files manually. Sounds great, right? It is! So why doesn’t Kitware want you to do it? Well, to put it simply; source control.
The issue with globbing is that no change has happen to the CMake file on merging or updating, so CMake will not know that it needs to regenerate. There is another way around this, fortunately. What I do is I glob, and then I have a script set to run on “update” action in my source control that will run CMake in the build directory. What this allows is for me to support globbing, and my solution/makefile to not go out-of-date.
1 2 3 4 |
# Glob and add a library target file(GLOB PROJECT_SRC *.cpp) file(GLOB PROJECT_HPP *.h *.hpp) add_library(my-library ${PROJECT_SRC} ${PROJECT_HPP}) |
And in my git hooks for this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Git Hook which, depending on user setting, will automatically run cmake autocmake=$(git config --bool hooks.autocmake) if [ "$autocmake" == "true" ]; then # Find CMake_____.txt depth | directory], sort by depth, only print directory, limit to first result sourcedirectory=$(find -name CMakeLists.txt -printf '%d\0%h\n' | sort -t '\0' -n | awk -F '\0' '{print $2}' | head -n 1) binarydirectory=$(find -name CMakeCache.txt -printf '%d\0%h\n' | sort -t '\0' -n | awk -F '\0' '{print $2}' | head -n 1) # If valid directories for both, run cmake in the proper directory if [[ "$binarydirectory" != "" && "$sourcedirectory" != "" ]]; then echo "Running cmake hook..." cd $binarydirectory $sourcedirectory cmake cmake $sourcedirectory cd - >& /dev/null fi fi |
If you are unfamiliar with git hooks (or whatever the equivalent would be for the source control of your choice) I highly recommend becoming accustom to them. Hooks are used to streamline many aspects of development.
Side-note: I read once on a site that passing in the headers to add_executable was bad form? I disagree. If you do not pass the headers into the target, the generated solution will not display headers (Visual Studio in particular). So I would recommend this practice, because it doesn’t hurt anyone, and only helps Windows users.
What About Precompiled Headers?
I’m glad you asked! Precompiled headers are really a mess, as they aren’t a part of the standard, but almost every compiler supports them. Since precompiled headers (PCH) are tied very closely with the code-base, and not really the build chain, it’s usually seen as “your problem”. Some build tools will take PCH into account (I believe Premake has functionality for it), but CMake does not, and for good reason. The real fix to build times is coming – but it’s still years and years down the road. Until we have binary modules in C++, we will have to use PCH.
Unfortunately, as mentioned, since PCH is such a case-by-case thing – it’s difficult to add to a build chain. What I’ve found to be the best method is to not account at all for PCH in the code itself (no #includes at the top of every file) and instead set a compiler option to force include the header if it’s there, and set other compilation flags as needed. This is best done by overriding the add_project and add_library functions in CMake (which is totally possible, but totally dangerous, because you can only override once since functions aren’t first-class citizens in CMake.)
Unfortunately I don’t have the “simple fix” just lying around somewhere. The way I deal with PCH is I don’t commit any of my PCH stuff, I parse source files, generate a PCH header and source files, and then run this function on all targets using the PCH Header and the source files before I pass them into add_executable or add_library. What is provided below is the simplified version of the code, stripped of the generation. You should be able to take this and work with it to create your own enable_precompiled_headers function that does what you need it to do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
################################################################################ ## enable_precompiled_headers ################################################################################ function(enable_precompiled_headers PRECOMPILED_HEADER SOURCE_VARIABLE_NAME) if(MSVC) set(files ${${SOURCE_VARIABLE_NAME}}) set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/pch_nue.cpp PROPERTIES COMPILE_FLAGS "/Yc\"${PRECOMPILED_HEADER}\"") set_source_files_properties( ${files} PROPERTIES COMPILE_FLAGS "/Yu\"${PRECOMPILED_HEADER}\" /FI\"${PRECOMPILED_HEADER}\"" ) set(${SOURCE_VARIABLE_NAME} ${PRECOMPILED_HEADER} ${CMAKE_CURRENT_BINARY_DIR}/pch_nue.cpp ${${SOURCE_VARIABLE_NAME}} PARENT_SCOPE) endif(MSVC) endfunction(enable_precompiled_headers) |
So maybe not the simplest answer that you were expecting, and it doesn’t work on multiple platforms (you can see how you would make it work though, add other if checks). But you don’t slow down someone who literally cannot build precompiled headers for whatever reason (because we don’t physically include in the file, instead we add a Force Include). A reason PCH might be undesired is because it’s slower if we’re doing a Unity build, which is very common for build servers. So don’t think for a minute your end-user just always wants PCH, that’s not always true.
Side-note: You should be able to build your code without PCH enabled, and no PCH file being included. If you cannot your code is poorly formed.
Side-note: I plan on someday soon sharing my fix which autogenerates PCH files by parsing the source code. So look forward to that at a later date!
More Tips and Tricks!
There are many more tricks I could teach, but I think that’s enough for now. These are just a few of the important things I’ve found to be super helpful when working on a project with CMake as my build system. I hope you are able to take some of them and try them out, I know they’ve helped me! If you have success with any of them or failure, please be sure to come back and talk about it, I’m excited to listen!
Cheers ~