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:
- The very helpful
$<IF:...,...,...>
expression is only available in CMake version >= 3.8 - You have to write it in a single line. To avoid it I used the
string(APPEND ...)
, which you can also use to "optimize" yourset(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ...
calls. - 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:
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 targetbar
PUBLIC
options will be applied to both, the original targetfoo
and the consuming targetbar
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
Why Does Omission of "#Include ≪String≫" Only Sometimes Cause Compilation Failures
Why Copy Constructor Is Not Called in This Case
How to Achieve the Theoretical Maximum of 4 Flops Per Cycle
How to Erase an Element from Std::Vector≪≫ by Index
What's the Difference Between Assignment Operator and Copy Constructor
Difference Between 'New Operator' and 'Operator New'
What Are the Errors, Misconceptions or Bad Pieces of Advice Given by Cplusplus.Com
Why Are Global and Static Variables Initialized to Their Default Values
How to Sort a Std::Vector by the Values of a Different Std::Vector
How to Create an Std::Function from a Move-Capturing Lambda Expression
Getting a File* from a Std::Fstream
Why Does Rand() Yield the Same Sequence of Numbers on Every Run
Why How to Use Auto on a Private Type
Significance of Ios_Base::Sync_With_Stdio(False); Cin.Tie(Null);
What Does 'Unsigned Temp:3' in a Struct or Union Mean
How to Safely Pass Objects, Especially Stl Objects, to and from a Dll