Modern Way to Set Compiler Flags in Cross-Platform Cmake Project

Modern way to set compiler flags in cross-platform cmake project

Your approach would - as @Tsyvarev has commented - be absolutely fine, just since you've asked for the "new" approach in CMake here is what your code would translate to:

cmake_minimum_required(VERSION 3.8)

project(HelloWorld)

string(
APPEND _opts
"$<IF:$<CXX_COMPILER_ID:MSVC>,"
"/W4;$<$<CONFIG:RELEASE>:/O2>,"
"-Wall;-Wextra;-Werror;"
"$<$<CONFIG:RELEASE>:-O3>"
"$<$<CXX_COMPILER_ID:Clang>:-stdlib=libc++>"
">"
)

add_compile_options("${_opts}")

add_executable(HelloWorld "main.cpp")

target_compile_features(HelloWorld PUBLIC cxx_lambda_init_captures)

You take add_compile_options() and - as @Al.G. has commented - "use the dirty generator expressions".

There are some downsides of generator expressions:

  1. The very helpful $<IF:...,...,...> expression is only available in CMake version >= 3.8
  2. You have to write it in a single line. To avoid it I used the string(APPEND ...), which you can also use to "optimize" your set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ... calls.
  3. It's difficult to read and understand. E.g. the semicolons are needed to make it a list of compile options (otherwise CMake will quote it).

So better use a more readable and backward compatible approach with add_compile_options():

if(MSVC)
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
else()
add_compile_options("-Wall" "-Wextra" "-Werror" "$<$<CONFIG:RELEASE>:-O3>")
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options("-stdlib=libc++")
else()
# nothing special for gcc at the moment
endif()
endif()

And yes, you don't explicitly specify the C++ standard anymore, you just name the C++ feature your code/target does depend on with target_compile_features() calls.

For this example I've chosen cxx_lambda_init_captures which would for e.g. an older GCC compiler give the following error (as an example what happens if a compiler does not support this feature):

The compiler feature "cxx_lambda_init_captures" is not known to CXX compiler

"GNU"

version 4.8.4.

And you need to write a wrapper script to build multiple configurations with a "single configuration" makefile generator or use a "multi configuration" IDE as Visual Studio.

Here are the references to examples:

  • Does CMake always generate configurations for all possible project configurations?
  • How do I tell CMake to use Clang on Windows?
  • How to Add Linux Compilation to Cmake Project in Visual Studio

So I've tested the following with the Open Folder Visual Studio 2017 CMake support to combine in this example the cl, clang and mingw compilers:

Configurations

CMakeSettings.json

{
// See https://go.microsoft.com//fwlink//?linkid=834763 for more information about this file.
"configurations": [
{
"name": "x86-Debug",
"generator": "Visual Studio 15 2017",
"configurationType": "Debug",
"buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
"buildCommandArgs": "-m -v:minimal",
},
{
"name": "x86-Release",
"generator": "Visual Studio 15 2017",
"configurationType": "Release",
"buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
"buildCommandArgs": "-m -v:minimal",
},
{
"name": "Clang-Debug",
"generator": "Visual Studio 15 2017",
"configurationType": "Debug",
"buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
"cmakeCommandArgs": "-T\"LLVM-vs2014\"",
"buildCommandArgs": "-m -v:minimal",
},
{
"name": "Clang-Release",
"generator": "Visual Studio 15 2017",
"configurationType": "Release",
"buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
"cmakeCommandArgs": "-T\"LLVM-vs2014\"",
"buildCommandArgs": "-m -v:minimal",
},
{
"name": "GNU-Debug",
"generator": "MinGW Makefiles",
"configurationType": "Debug",
"buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
"variables": [
{
"name": "CMAKE_MAKE_PROGRAM",
"value": "${projectDir}\\mingw32-make.cmd"
}
]
},
{
"name": "GNU-Release",
"generator": "Unix Makefiles",
"configurationType": "Release",
"buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
"variables": [
{
"name": "CMAKE_MAKE_PROGRAM",
"value": "${projectDir}\\mingw32-make.cmd"
}
]
}
]
}

mingw32-make.cmd

@echo off
mingw32-make.exe %~1 %~2 %~3 %~4

So you can use any CMake generator from within Visual Studio 2017, there is some unhealthy quoting going on (as for September 2017, maybe fixed later) that requires that mingw32-make.cmd intermediator (removing the quotes).

Correct way to handle compiler flags when using CMake

The proper way to set flags is with set_compile_options and target_compile_options and macros with add_compile_definitions and target_compile_definitions. You should not (or rarely) touch CMAKE_*_FLAGS yourself and with the creation of generator expressions, you rarely should touch CMAKE_*_FLAGS_*, too. Using $<CONFIG:RELEASE> is simpler, because you don't need to care about case (-DCMAKE_BUILD_TYPE=Release and -DCMAKE_BUILD_TYPE=rElEaSe are both release builds) and for my eyes much cleaner to read.

Do in your main CMakeLists.txt:

add_compile_options(
-Wall -Werror -Wno-error=maybe-uninitialized
$<$<CONFIG:RELEASE>:-Ofast>
$<$<CONFIG:DEBUG>:-O0>
$<$<CONFIG:DEBUG>:-ggdb3>
)
add_compile_definitions(
$<$<CONFIG:RELEASE>:NDEBUG>
$<$<CONFIG:RELEASE>:BOOST_DISABLE_ASSERTS>
)
add_subdirectory(WorkerLibFolder)
add_subdirectory(TestsFolder)
add_executable(main ...)

I want to alway have -O0 being set when compiling test environment

So do exactly that. Inside TestsFolder do:

add_compile_options(-O0)
add_library(test_environment ....)

Or better:

add_library(test_environment ..
target_compile_options(test_environment PRIVATE -O0)

Because compile options are accumulated, the options added in TestsFolder will be suffixed/the last options on the compile line so it will work, I mean ex. gcc -O3 -Ofast -O0 will compile the same as gcc -O0.

target_include_directories(worker PUBLIC ${CMAKE_CURRENT_LIST_DIR})

The CMAKE_CURRENT_LIST_DIR is usually used from include(this_files) files to indicate from where the file is. To include current directory the CMAKE_CURRENT_SOURCE_DIR is more commonly used which, well, indicates the current source directory cmake is processing.

PS: I wouldn't unittest a program/library with different optimize options then the release is build with. I unittest with exactly the same compile options as the release builds. Some bugs show up only with optimizations enabled. The way I preferably unittest a (previous) library is to compile and unittest with both optimization disabled and enabled.

What is the modern method for setting general compile flags in CMake?

For modern CMake (versions 2.8.12 and up) you should use target_compile_options, which uses target properties internally.

CMAKE_<LANG>_FLAGS is a global variable and the most error-prone to use. It also does not support generator expressions, which can come in very handy.

add_compile_options is based on directory properties, which is fine in some situations, but usually not the most natural way to specify options.

target_compile_options works on a per-target basis (through setting the COMPILE_OPTIONS and INTERFACE_COMPILE_OPTIONS target properties), which usually results in the cleanest CMake code, as the compile options for a source file are determined by which project the file belongs to (rather than which directory it is placed in on the hard disk). This has the additional advantage that it automatically takes care of passing options on to dependent targets if requested.

Even though they are little bit more verbose, the per-target commands allow a reasonably fine-grained control over the different build options and (in my personal experience) are the least likely to cause headaches in the long run.

In theory, you could also set the respective properties directly using set_target_properties, but target_compile_options is usually more readable.

For example, to set the compile options of a target foo based on the configuration using generator expressions you could write:

target_compile_options(foo PUBLIC "$<$<CONFIG:DEBUG>:${MY_DEBUG_OPTIONS}>")
target_compile_options(foo PUBLIC "$<$<CONFIG:RELEASE>:${MY_RELEASE_OPTIONS}>")

The PUBLIC, PRIVATE, and INTERFACE keywords define the scope of the options. E.g., if we link foo into bar with target_link_libraries(bar foo):

  • PRIVATE options will only be applied to the target itself (foo) and not to other libraries (consumers) linking against it.
  • INTERFACE options will only be applied to the consuming target bar
  • PUBLIC options will be applied to both, the original target foo and the consuming target bar

How to set compiler options with CMake in Visual Studio 2017

The default settings for the compiler are picked up from standard module files located in the Modules directory of the CMake installation. The actual module file used depends on both the platform and the compiler. E.g., for Visual Studio 2017, CMake will load the default settings from the file Windows-MSVC.cmake and language specific settings from Windows-MSVC-C.cmake or Windows-MSVC-CXX.cmake.

To inspect the default settings, create a file CompilerOptions.cmake in the project directory with the following contents:

# log all *_INIT variables
get_cmake_property(_varNames VARIABLES)
list (REMOVE_DUPLICATES _varNames)
list (SORT _varNames)
foreach (_varName ${_varNames})
if (_varName MATCHES "_INIT$")
message(STATUS "${_varName}=${${_varName}}")
endif()
endforeach()

Then initialize the CMAKE_USER_MAKE_RULES_OVERRIDE variable in your CMakeLists.txt:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
set (CMAKE_USER_MAKE_RULES_OVERRIDE "${CMAKE_CURRENT_LIST_DIR}/CompilerOptions.cmake")
project(foo)
add_executable(foo foo.cpp)

When the project is configured upon opening the directory using Open Folder in Visual Studio 2017, the following information will be shown in the IDE's output window:

 ...
-- CMAKE_CXX_FLAGS_DEBUG_INIT= /MDd /Zi /Ob0 /Od /RTC1
-- CMAKE_CXX_FLAGS_INIT= /DWIN32 /D_WINDOWS /W3 /GR /EHsc
-- CMAKE_CXX_FLAGS_MINSIZEREL_INIT= /MD /O1 /Ob1 /DNDEBUG
-- CMAKE_CXX_FLAGS_RELEASE_INIT= /MD /O2 /Ob2 /DNDEBUG
-- CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT= /MD /Zi /O2 /Ob1 /DNDEBUG
...

So the warning setting /W3 is picked up from the CMake variable CMAKE_CXX_FLAGS_INIT which then applies to all CMake targets generated in the project.

To control the warning level on the CMake project or target level, one can alter the CMAKE_CXX_FLAGS_INIT variable in the CompilerOptions.cmake by adding the following lines to the file:

if (MSVC)
# remove default warning level from CMAKE_CXX_FLAGS_INIT
string (REGEX REPLACE "/W[0-4]" "" CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT}")
endif()

The warning flags can then be controlled by setting the target compile options in CMakeLists.txt:

...
add_executable(foo foo.cpp)
target_compile_options(foo PRIVATE "/W4")

For most CMake projects it makes sense to control the default compiler options in a rules override file instead of manually tweaking variables like CMAKE_CXX_FLAGS.

When making changes to the CompilerOptions.cmake file, it is necessary to recreate the build folder. When using Visual Studio 2017 in Open Folder mode, choose the command Cache ... -> Delete Cache Folders from the CMake menu and then Cache ... -> Generate from the CMake menu to recreate the build folder.

How to set compiler specific flags in cmake

You can set the variables CMAKE_CXX_FLAGS_RELEASE (applies to release builds only), CMAKE_CXX_FLAGS_DEBUG (applies to debug builds only) and CMAKE_CXX_FLAGS (applies to both release and debug).
In case you use also other compilers, you should only set the options for msvc:

if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:latest")
endif()

CMake: Set compiler flags in way that can be changed by user.

So, by experimenting I figured this out (sort of).

First I found out that the CACHE version does not work because there already is value in cache. That is if I apply FORCE at end of it it gets set:

SET( CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -DNVALGRIND" CACHE STRING "" FORCE )

obviously this would not allow users to specify flags by themselves, making it equivalent to first option.

The solution is:

Put cache setting command just at the beginning of cmake file (before project command), somehow this sets values before cmake sets them internally. So now it looks like this:

SET( CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -DNVALGRIND" CACHE STRING "" )
SET( CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG -DNVALGRIND" CACHE STRING "" )
...
project( whatever )
...

and it works. I guess that this will be bad if you use compilers which require some different default flags. But then you should not set default by yourself anyway.

I'm still wondering if there is cleaner way.

Setting Various compilers in CMake for creating a shared library

CMake allows one compiler per language, so simply writing this is enough:

cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX CUDA)

add_subdirectory(Cuda)
add_subdirectory(SYCL)

You can separately set the C++ and CUDA compilers by setting CMAKE_CXX_COMPILER and CMAKE_CUDA_COMPILER at the configure command line.

$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER=g++ -DCMAKE_CUDA_COMPILER=nvcc

Also, I want to clear up this misconception:

Each Folder(Cuda and SYCL) can have their dedicated CmakeLists.txt which would specify the compiler and the various flags to go with it.

The CMakeLists.txt file should not attempt to specify the compiler. It's tricky to do correctly, can't always be done (especially in the add_subdirectory case) and unnecessarily restricts your ability to switch out the compiler. Maybe you have both GCC 10 and 11 installed and want to compare the two.

Similarly, you should not specify flags in the CMakeLists.txt file that aren't absolutely required to build, and you should always check the CMake documentation to see if the flags you're interested in have been abstracted for you. For instance, CMake has special handling of the C++ language standard (via target_compile_features) and CUDA separable compilation (via the CUDA_SEPARABLE_COMPILATION target property).

The best solution, as I have detailed here, is to set optional flags via the *_FLAGS* variables in a preset or toolchain.



Related Topics



Leave a reply



Submit