Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

cmake

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:

  1. First the trivial: Is there relly no command like appen that would allow me to replace set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} Foo") with append(CMAKE_CXX_FLAGS "Foo")?
  2. 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.
  3. 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.

So my questions are

  • a) Is there a better way to write th cmake script that solves my "pain points"? solution to the points mentioned above?
  • b) Is there something like an accepted, modern best practice of how to set up such projects?

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.

like image 768
MikeMB Avatar asked Aug 30 '17 08:08

MikeMB


2 Answers

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).

like image 61
Florian Avatar answered Oct 03 '22 19:10

Florian


Don't do it!

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:

  1. There are too many compiler vendors. There's MSVC, Clang, and GCC, yes, but there's also the Intel compiler, the PGI compiler, and so on.
  2. There are too many compiler variants. Not only is there Clang, there's also ClangCL and the Clang CUDA compiler. The Intel compiler can also switch between MSVC and GCC compatible modes.
  3. There are too many compiler versions. The meanings of warning flags change across versions. From one version to the next, a given warning might be more or less sensitive, especially the more advanced ones that perform dataflow analysis. With warnings-as-errors enabled, this translates into broken builds for your users.

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.

like image 30
Alex Reinking Avatar answered Oct 03 '22 18:10

Alex Reinking