I want to write a cmake file that sets different compiler options for clang++, g++ and MSVC in debug and release builds. What I'm doing currently looks something like this:
if(MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:c++latest /W4") # Default debug flags are OK set(CMAKE_CXX_FLAGS_RELEASE "{CMAKE_CXX_FLAGS_RELEASE} /O2") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1z -Wall -Wextra -Werror") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} some other flags") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") else() # nothing special for gcc at the moment endif() endif()
But I have a couple of problems with this:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} Foo")
with append(CMAKE_CXX_FLAGS "Foo")
? CMAKE_CXX_FLAGS
and similar variables in the first place, but im not sure what other mechanism to use.make foo_debug_clang
.So my questions are
Most references I could find on the internet are either out of date or show only trivial examples. I currently using cmake3.8, but if that makes any difference, I'm even more interested in the answer for more recent versions.
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:
$<IF:...,...,...>
expression is only available in CMake version >= 3.8string(APPEND ...)
, which you can also use to "optimize" your set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ...
calls.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:
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).
Especially not in CMake 3.19+ where presets are an option. Put optional settings like warning flags in the presets and put only hard build requirements in the CMakeLists.txt.
Read on for an explanation why.
I want to write a cmake file that sets different compiler options for clang++, g++ and MSVC in debug and release builds.
Here's the thing: you don't want to write a CMakeLists.txt that sets different options, you just want to have a convenient place to store your flags. That's where presets and toolchain files come in (more below).
What I'm doing currently looks something like this:
if(MSVC) # ... else() # ... if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") # ... else() # ... endif() endif()
[...] I've read multiple times, that one should not manually set CMAKE_CXX_FLAGS and similar variables in the first place, but im not sure what other mechanism to use.
Here's the problems with this structure:
You do not want to be in the business of maintaining flag compatibility tables for your build. Fortunately, there's a simple solution: don't.
Store your intended flags in a preset (CMake 3.19+) or a toolchain file (CMake 3.0+, maybe earlier) and let your users opt-in to those settings if they so choose.
With a preset, it's as simple as writing a CMakePresets.json file. There are some extensive examples in the documentation. Then you let your users tell you which set of flags they want to use:
# Using presets: $ cmake --preset=msvc $ cmake --preset=gcc $ cmake --preset=clang # Using toolchains: $ cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/msvc.cmake $ cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/gcc.cmake $ cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/clang.cmake
and so on.
Most importantly: The way I do it here, I need a separate build directory for each compiler and configuration Ideally I'd like to transform that into havin multiple targets in the same directory so I can e.g. call
make foo_debug_clang
.
This is an odd requirement because each one of those has to fully build everything anyway. I would honestly just recommend setting up a wrapper Makefile to support this workflow because (a) CMake doesn't support it natively and (b) there's no real advantage to extending CMake to do so.
- b) Is there something like an accepted, modern best practice of how to set up such projects?
I don't know about "accepted", but I have absolutely found that splitting the hard build requirements from the ideal build settings between the CMakeLists.txt and the presets (or toolchain files) respectively solves (or sidesteps) many of these sorts of issues. The end result is more robust builds that are easier to work with.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With