6

I have seen the following way of setting CMAKE_CXX_FLAGS in the toolchain file:

SET(CMAKE_CXX_FLAGS "-m32" CACHE STRING "C++ compiler flags" FORCE)

Should I use it in the toolchain file instead of

SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32")

?

What are differences between them ?

Irbis
  • 1,432
  • 1
  • 13
  • 39
  • If you're the type of person that prefers `CMakeLists.txt` to contain only the minimum required to compile the project, another option to consider is setting them through [preset files](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html). – Mark G Jun 08 '22 at 17:55
  • Does the second way - without CACHE - ever work inside a **toolchain**? According to [that question](https://stackoverflow.com/questions/11423313/cmake-cross-compiling-c-flags-from-toolchain-file-ignored), it is not. – Tsyvarev Jun 08 '22 at 18:33
  • The second way works. Tested using cmake 3.23.2. Builded binaries have been verified using ```file``` command. – Irbis Jun 08 '22 at 21:10
  • The second way does _not_ work in general. It is in fact very broken. See my answer for more details. – Alex Reinking Jun 23 '22 at 11:40

2 Answers2

6

tl;dr: there are two acceptable ways of doing this.

First, and most of the time (90%+), you can use the _INIT variables as suggested by the documentation:

set(CMAKE_CXX_FLAGS_INIT "-m32")

Second, if CMake is adding incorrect/conflicting flags for your compiler/platform combination, you can override it completely by setting the cache variable without FORCE.

set(CMAKE_CXX_FLAGS "-m32" CACHE STRING "C++ compiler flags")

Read on for further details.


Let's run a few experiments. We'll use the following CMakeLists.txt:

cmake_minimum_required(VERSION 3.23)
project(test LANGUAGES CXX)

message(STATUS "CMAKE_CXX_FLAGS_DEBUG = ${CMAKE_CXX_FLAGS_DEBUG}")

On most systems, CMake leaves CMAKE_CXX_FLAGS blank by default. The main exception is Windows with MSVC, where it adds /EHsc and (on older versions) /GR to ensure that standard C++ exception handling and RTTI are enabled.

Since I don't have ready access to a Windows system, I use CMAKE_CXX_FLAGS_DEBUG, which does have default-initialized flags on most compilers. The same principles apply, though, since it is the responsibility of the platform module to set these in both cases.

Experiment 1: No toolchain file

$ cmake -S . -B build
-- The CXX compiler identification is GNU 11.2.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- CMAKE_CXX_FLAGS_DEBUG = -g
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/build

So on this compiler, CMAKE_CXX_FLAGS_DEBUG is set to -g. This is our baseline.

Experiment 2: Set-cache with force

Now we'll create a toolchain file called set-cache-force.cmake:

# set-cache-force.cmake
set(CMAKE_CXX_FLAGS_DEBUG "-DMY_DEBUG" CACHE STRING "C++ compiler flags" FORCE)

We'll configure the project with this toolchain:

$ rm -rf build
$ cmake -S . -B build --toolchain set-cache-force.cmake 
...
-- CMAKE_CXX_FLAGS_DEBUG = -DMY_DEBUG
...

As we can see, the original -g flag was suppressed and the -DMY_DEBUG cache value "won". Of course, this isn't really a debug mode anymore, which should illustrate why overriding all the flags isn't always what we want.

Even worse, using FORCE here disables a user's ability to override CMAKE_CXX_FLAGS_DEBUG themselves:


$ rm -rf build
$ cmake -S . -B build --toolchain set-cache-force.cmake -DCMAKE_CXX_FLAGS_DEBUG="-DOVERRIDE"
...
-- CMAKE_CXX_FLAGS_DEBUG = -DMY_DEBUG
...

This is highly undesirable behavior. A user would need to edit your toolchain file to work around a bug or add further customizations.

Experiment 3: Set-cache without force

If we run the same experiment as before without FORCE setting it, then we still get the same flags, but we retain the ability to incrementally override the toolchain file.

# set-cache.cmake
set(CMAKE_CXX_FLAGS_DEBUG "-DMY_DEBUG" CACHE STRING "C++ compiler flags")

Now we can see that it works:

$ rm -rf build
$ cmake -S . -B build --toolchain set-cache.cmake
...
-- CMAKE_CXX_FLAGS_DEBUG = -DMY_DEBUG
...

And that it can still be overridden:

$ rm -rf build
$ cmake -S . -B build --toolchain set-cache.cmake -DCMAKE_CXX_FLAGS_DEBUG="-DOVERRIDE"
...
-- CMAKE_CXX_FLAGS_DEBUG = -DOVERRIDE
...

And it can even be overridden again:

$ cmake -S . -B build -DCMAKE_CXX_FLAGS_DEBUG="-DOVERRIDE2"
...
-- CMAKE_CXX_FLAGS_DEBUG = -DOVERRIDE2
...

Experiment 4: Set normal variable

Now we'll try to set this as a normal variable. Again, we'll create a toolchain file called set-normal.cmake:

# set-normal.cmake
set(CMAKE_CXX_FLAGS_DEBUG "-DMY_DEBUG")

Again, running this shows that -DMY_DEBUG "wins", overriding CMake's default flags:

$ cmake -S . -B build --toolchain set-normal.cmake
...
-- CMAKE_CXX_FLAGS_DEBUG = -DMY_DEBUG
...

Like experiment 2, this prevents users from overriding it... bad!

$ cmake -S . -B build --toolchain set-normal.cmake -DCMAKE_CXX_FLAGS_DEBUG="-DOVERRIDE"
...
-- CMAKE_CXX_FLAGS_DEBUG = -DMY_DEBUG
...

Experiment 5: Append normal variable

Now we'll try with the code in your post. Again, we'll use a toolchain called append-normal.cmake:

# append-normal.cmake
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DMY_DEBUG")

Now we get a very different result:

$ rm -rf build
$ cmake -S . -B build --toolchain append-normal.cmake
...
-- CMAKE_CXX_FLAGS_DEBUG =  -DMY_DEBUG -DMY_DEBUG
...

This is just completely wrong! What happened here? Well, the toolchain file gets read multiple times during project initialization, and here this causes the -DMY_DEBUG flag to be appended twice. At least that's what happens on the first run:

$ cmake -S . -B build
...
-- CMAKE_CXX_FLAGS_DEBUG = -g -DMY_DEBUG
...

After the first run, the CMake default gets cached and so we append to that on subsequent runs. Furthermore, CMake only reads your toolchain file once now.

You must always make your toolchain files idempotent. That means that running it twice does the same thing as running it once.

Experiment 6: Using _INIT variables

This is the developer-intended way of doing things per the documentation. See the documentation here: https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS_INIT.html

Value used to initialize the CMAKE_<LANG>_FLAGS cache entry the first time a build tree is configured for language <LANG>. This variable is meant to be set by a toolchain file. CMake may prepend or append content to the value based on the environment and target platform.

Now we use a toolchain file called init-var.cmake:

# init-var.cmake
set(CMAKE_CXX_FLAGS_DEBUG_INIT "-DMY_DEBUG")

And we re-run the build:

$ rm -rf build
$ cmake -S . -B build --toolchain init-var.cmake
...
-- CMAKE_CXX_FLAGS_DEBUG = -DMY_DEBUG -g
...

Now we can see that CMake appended its default flags to the initial ones we provided. And indeed this still allows users to override things:

$ cmake -S . -B build --toolchain init-var.cmake -DCMAKE_CXX_FLAGS_DEBUG="-DOVERRIDE"
...
-- CMAKE_CXX_FLAGS_DEBUG = -DOVERRIDE
...

In my experience, 90%+ of the time it's correct to let CMake add in its extra flags using Experiment 6 (the _INIT variables). But every so often you'll want to completely override CMake using Experiment 3 (set(CACHE) without FORCE).

What you do not want to do is anything that behaves differently on subsequent runs (like experiment 5) or that disables key CMake functionality (ie. respecting the cache variable, like experiments 2 and 4).

Alex Reinking
  • 16,724
  • 5
  • 52
  • 86
-1

When you use set(variable "value" CACHE STRING "..." FORCE), the variable is set for all the projects built in the current session (including those that are in the sub-directories).

But simply using set(variable "value") without the cache part only adds the flags for the immediate project scope (current CMakeLists.txt) and not the upper directories that have their own CMakeLists.txt.

Amin Ya
  • 1,515
  • 1
  • 19
  • 30