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

5

46

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.

Heteropolar answered 30/8, 2017 at 8:14 Comment(7)
About the first two points: cmake.org/cmake/help/v3.3/command/add_compile_options.htmlHumic
@AI.G. Thank you. Are there also separate sets for debug and release it to I have to solve that via another level of if_else? I experimented a bit with generator expressions, but I got the feeling that this becomes even less readable.Heteropolar
As I see it you can either use the dirty generator expressions or the rather verbose syntax of set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} --flags"). In this case (considering both of these unsatisfactory) you could try to write some helper functions wrapping the dirty code into something more pleasant for the eye.Humic
I've read multiple times, that one should not manually set CMAKE_CXX_FLAGS - First time hear about that. While setting cached version of this variable in the project could be inconvinient for the user (but useful in some cases), there is nothing wrong in appending flags to non-cached variable. I need a separate build directory for each compiler - This is unavoidable both in CMake and many build tools it uses (e.g. MSVC). As for single build directory for several configurations, multiconfig build tools (like MSVC) are able to work in such way.Julietajulietta
The whole question seems "to broad" for me, as points 1,2 are weakly correlate with 3. May be, ask specifically about 3, and add 1,2 as sidenotes (so 1,2 wouldn't need to be described in answers)?Julietajulietta
@Tsyvarev: Maybe you are right, but to clarify: My questions are what I now call a) and b) - not 1,2,3 (although they are of course part of it). The thing is,I'm not looking for a piecmeal solution - thats why I asked for an established best practice advise. Question a) and the specific complaints 1-3 are more of an fallback for the (expected) case that no such practice exists. And btw.: boost's build system allows me to specify multiple different toolchains together and I hoped cmake had by now gained a similar mechanism.Heteropolar
@Tsyvarev: Regarding not setting CMAKE_CXX_FLAGS specifically I heard mostly advise about e.g. not specifying link commands manually or not setting the language standard this way and instead using the more "High Level" cmake functions. For linking/dependency management, target_link_libraries works pretty well,but for most other things (language level, warning level, sanitizer etc.) the cmake default mechanisms are imho just not good enough. However, the advise from @Al.G about add_compile_options has at least that it si easier to specify different flags for different targets.Heteropolar
F
24

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:

So I've tested the following with the Open Folder Visual Studio 2017 CMake support to combine in this example the , and 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).

Fante answered 9/9, 2017 at 15:13 Comment(6)
I'm strictly against specifying individual standard language features requirements instead of specifying the standard I'm coding against, but otherwise thanks for the comprehensive answer.Heteropolar
@Heteropolar You're welcome. You can use a general declaration also with target_compile_features(cxx_std_17).Fante
I'm not totally happy with the result, but that is definetively more cmake's fault than yours. Btw.: Specifying cxx_std_XX is better than specifying each feature, but afaik cmake resolves this to the -std=gnuc++XX switches instead of c++XXwhich are imho problematic for crossplatform development. Usually I try to bring all compilers as close to standard conformance as possible (-std=c++XX, /permissive-, -pedantic etc.) and then backoff if certain 3rd party libraries won't compile under them, but that is beyond the scope of the question.Heteropolar
@Heteropolar You get the more restrictive -std=c++XX option by setting CMAKE_CXX_EXTENSIONS to OFF.Fante
Thanks, didn't knwo about that oneHeteropolar
I completely disagree that warning flags belong in the CMakeLists.txt at all. If you upgrade (or downgrade) your compiler relative to the one used to author the project, the sensitivity of those warnings is likely to change. With -Werror in the mix, that will take your project from working to not working in a flash.Tusker
T
19

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.

Tusker answered 23/6, 2021 at 6:31 Comment(9)
As far as I understand from the documentation, presets are only for developers and CI. But an option -Werror is useful for the end user. Also, presets are for the case, when a developer knows his/her environment. But an option -Werror is useful for running in unknown environment, with a compiler of "unknown version": When such compiler finds something strange with the project's code, it is much better to terminate building than allowing a compiler to create a program which is possibly wrong.Julietajulietta
@Julietajulietta - The documentation does not say that. I don't know why you chose to make that up. CI was given as an example, but nowhere does it say anything like "only for". Please don't engage me in bad faith. The -Werror option is maximally anti-user. Old code is likely to have bugs, new compilers are marginally more likely to catch them. Some warnings are spurious. Making an end user edit the build files just to make the build go through is the absolute worst case scenario for a build.Tusker
If I write a library and someone else wants to build it from source using an earlier compiler version than I'm using, that should be allowed. If I force -Werror on them, then I become responsible for testing my code on myriad old compiler versions, even if a bug is just with a warning. Or, I force them to patch my build, which is a no-go.-Werror is shockingly and aggressively user-hostile and has no place being hard-coded.Tusker
@Julietajulietta - This point is not even controversial... blog.schmorp.de/… embeddedartistry.com/blog/2017/05/22/werror-is-not-your-friend flameeyes.blog/2009/02/25/… github.com/catchorg/Catch2/issues/1152 devmanual.gentoo.org/ebuild-writing/common-mistakes/index.htmlTusker
"If I force -Werror on them, then I become responsible for testing my code on myriad old compiler versions, even if a bug is just with a warning." - The project's author(s) is responsible for test the code on many compilers in any case. It is always a trade-off, whether to allow the user to build the project on untested configuration or not. It is always a trade-off whether to continue building after the compiler warnings or not. See e.g. that comment to the blogpost you reference.Julietajulietta
And still, I cannot understand how presets could help in checking for compiler warnings/errors except for CI or on developers machines. Or, do you describe presets only for multiple configuration problem, which is stated as 3d question in the question post?Julietajulietta
We are not discussing this in the context of a tightly controlled corporate environment where exactly one compiler and version will be used at any given point in time. OP started from the desire to support a range of compilers. There is no merit to preventing your would-be users from building with a different compiler version. That's a great way to alienate your users.Tusker
Presets solve the problem of managing which flags should be used for which compilers in various scenarios (development, packaging, CI, etc.). For each tested scenario, a preset. For untested scenarios, which due to combinatorial explosion there will be many, a user may override a preset piecemeal or configure your build from a core that contains only requirements, not brittle cruft like -Werror.Tusker
@Julietajulietta - the anonymous reply right beneath the comment you linked has a good response: "I think it’s important to distinguish “hardcoding -Werror into your build system” vs “enabling -Werror on your CI with its known set of compilers.” The former can actualy cause errors if a user tries to build with an unusual compiler (e.g. PGI, Intel). The latter ensures that no warnings creep in under certain known conditions."Tusker
U
3

Addressing the first two points, but not the third:

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

The command you want is set_property. CMake supports a bunch of properties - not everything, but lots - in a way which saves you the trouble of doing compiler-specific work. For example:

set_property(TARGET foo PROPERTY CXX_STANDARD 17)

which for some compilers will result in --std=c++17 but for earlier ones with --std=c++1z (before C++17 was finalized). or:

set_property(TARGET foo APPEND PROPERTY COMPILE_DEFINITIONS HELLO WORLD)

will result it -DHELLO -DWORLD for gcc, clang and MSVC but for weird compilers might use other switches.

Is there relly no command like append that would allow me to replace set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} Foo") with append(CMAKE_CXX_FLAGS "Foo")?

set_property can be used either in set mode, or in append mode (see above examples).

I can't say whether this is preferable to add_compile_options or target_compile_features, though.

Unseasoned answered 1/2, 2018 at 20:39 Comment(0)
F
1

Another way is to use .rsp files.

set(rsp_file "${CMAKE_CURRENT_BINARY_DIR}/my.rsp")
configure_file(my.rsp.in ${rsp_file} @ONLY)
target_compile_options(mytarget PUBLIC "@${rsp_file}")

which might make the inclusion of multiple and esoteric options easier to manage.

Fonz answered 30/8, 2017 at 8:50 Comment(2)
I have to admit, I never heard of rsp_files - can you give an example? In any case, I'm not sure if splitting up the infromation that is now together at a single location into multiple separate files makes the process much better.Heteropolar
@Heteropolar an rsp file (response file) is just a means of putting command line options into a file rather than on the command line. All compilers I can think of support themFonz
W
1

You can use target_compile_options() to "append" compile options.

Wholewheat answered 30/8, 2017 at 11:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.