5

I am struggling to find a proper way to propagate correct compiler flags for all targets.

Let's imagine that there is a project which contains a library and unit tests.

ProjectFolder
|-WorkerLibFolder
  |-Worker.cpp
  |-Worker.hpp
  |-CMakeLists.txt  (2)
|-main.cpp
|-CMakeLists.txt (1)
|-TestsFolder
  |-UnitTests.cpp
  |-CMakeLists.txt  (3)

In CMakeLists.txt (1) I'd like to set compile options globally because I assume that optimization level and other flags should be the same for all libraries of the project. add_compile_options(-Wall -Werror -Wno-error=maybe-uninitialized) is used to achieve it.

Also I use CMAKE_BUILD_TYPE feature which automatically sets needed optimization level with help of CMAKE_CXX_FLAGS_RELEASE or CMAKE_CXX_FLAGS_DEBUG flags.

Also the fact that afterwards one of these variables is implicitly passed to the compiler seems to be annoying, becaues I anyway have to set these variables in advance for needed optimization flags set(CMAKE_CXX_FLAGS_RELEASE "-Ofast -DNDEBUG -DBOOST_DISABLE_ASSERTS") only because -O3 is by defaut set with Release configuration.

Anyway everything is ok up to the moment when I want to alway have -O0 being set when compiling test environment (UniTests.cpp). CMakeLists.txt (3) produces project_ut executable. I want WorkerLib to be compiled with configured optimization level (taked from CMAKE_BUILD_TYPE), but it automatically means that CMAKE_CXX_FLAGS_RELEASE with -Ofast is propagated to UnitTest.cpp

I guess that I might be doing something strange here and there is a better way to deal with an issue. One option here is to pass optimization flags without help of CMAKE_BUILD_TYPE feature but it seems to be a wrong (as I do not want to maintain lists of flags for every target).

EDIT:

#CMakeLists.txt(1):
cmake_minimum_required(VERSION 3.14.1)
project(TestProject LANGUAGES CXX C)

#####  common comp flags #####

add_compile_options(-Wall -Werror -Wno-error=maybe-uninitialized)

##############################

set(RELEASE_FLAGS "-Ofast -DNDEBUG -DBOOST_DISABLE_ASSERTS")
set(DEBUG_FLAGS "-O0 -ggdb3")

set(CMAKE_CXX_FLAGS_RELEASE ${RELEASE_FLAGS})
set(CMAKE_C_FLAGS_RELEASE ${RELEASE_FLAGS})

set(CMAKE_CXX_FLAGS_DEBUG ${DEBUG_FLAGS})
set(CMAKE_C_FLAGS_DEBUG ${DEBUG_FLAGS})

add_subdirectory(WorkerLibFolder)
add_subdirectory(TestsFolder)


add_executable(mainExec main.cpp)
target_link_libraries(mainExec PRIVATE worker)

#CMakeLists.txt(3):
add_executable(tests UnitTests.cpp)
target_link_libraries(tests PRIVATE worker)
#I want sommehow fix optimization flags for that particular target, while for worker library left them as they were set
#CMakeLists.txt(2):
add_library(worker Worker.cpp)
target_include_directories(worker PUBLIC ${CMAKE_CURRENT_LIST_DIR})
NwMan
  • 187
  • 1
  • 2
  • 10
  • 1
    It would be way-way cleaner rather then _describing_ what you do, to _show_ the actual CMakeLists.txt sources.. – KamilCuk Nov 29 '19 at 16:18
  • Agreed, it is difficult to decipher what it going on here based on the description. A minimal example would be much cleaner, and easier to understand. – Kevin Nov 29 '19 at 16:39
  • That's a good point, I added an example. Initially I somehow thought that description would be more preferable. – NwMan Nov 29 '19 at 17:14

2 Answers2

12

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.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • I tried to use what you suggested and it works and looks nice to me, though I have some comments: 1) compilation command now looks a bit strange: `-O3 -DNDEBUG -Wall -Werror -Wno-error=maybe-uninitialized -Ofast` for Worker library and `-O3 -DNDEBUG -Wall -Werror -Wno-error=maybe-uninitialized -Ofast -O0 ` for UnitTest executable. It's a pitty I cannot get rid of initial O3 & Ofast (Release build). 2) Optimization level is set only for test environment. I had to do it as there are some huge test data vectors and it takes ages to compile them in Release. – NwMan Nov 29 '19 at 17:46
  • 1) Yes. Sadly the `add_compile_options` and `target_compile_options` are append-only. There are some cmake scripts I've seen online to remove compile options from (even existing) targets or sources, but it get's messy real quick. 2) Yes, the `target_compile_options` let's us to specifically list compile options for each targets. Was that your intention to set optimization only for test environment? – KamilCuk Nov 29 '19 at 18:58
  • regarding point 2: as my test environment libraries are placed in a separate directory then I thought that `add_compile_options` can deal with it. But actually now I'm think of creating two function like `set_common_compile_options(target_name)` and `set_common_compile_options_for_test_env(target_name)` and call them for every target I have (the only drawback here I think is that I have to remember about calling this function each time). – NwMan Dec 02 '19 at 10:09
  • There's a typo here. `$<$-O0>` should be `$<$:-O0>`. Same for the `-ggdb3` one. – Martin Feb 16 '22 at 14:01
1

There are several recent best practices (here, here and here) that suggest your CMakeLists.txt should be clean and flexible.

I would suggest moving those flags externally (i.e. not placed in your CMakeLists.txt), by keeping them in related bash scripts, e.g.

C_FLAGS=""
C_FLAGS="${C_FLAGS} -Wall"
C_FLAGS="${C_FLAGS} -g -gline-tables-only"

LINKER_FLAGS=""

BUILD_TYPE=Debug

#

cmake \
  -GNinja \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_CXX_COMPILER=clang++ \
  -DCMAKE_POLICY_DEFAULT_CMP0056=NEW \
  -DCMAKE_EXPORT_COMPILE_COMMANDS=On \
  -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
  -DCMAKE_C_FLAGS="${C_FLAGS}" \
  -DCMAKE_EXE_LINKER_FLAGS="${LINKER_FLAGS}" \
  -DCMAKE_SHARED_LINKER_FLAGS="${LINKER_FLAGS}" \
  -DCMAKE_MODULE_LINKER_FLAGS="${LINKER_FLAGS}" \
  "${SRC_DIR}"

This way you can have sets of flags for what you are trying to do.

A next step would be to allow for generator expressions and configure per target for specific options per build type, but it is hard to suggest without seeing an example from your CMakeLists.txt.

You could even have project-specific CMake variables that can be influenced and used per target, but that might be too complicated for your project.

compor
  • 2,239
  • 1
  • 19
  • 29
  • Probably I should pay more attention to generator expresions after all and not creating external flag sets in an additional script. Thanks for the suggestion. – NwMan Nov 29 '19 at 17:51
  • @NwMan you should use them in combination. no weird compilation commands here. you'll be able to build a lot of stuff without hardcoding options in your CMake file, which is a CMake "[smell](https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/#leave-cmake_cxx_flags-alone)" and also follows best [practice](https://github.com/boost-cmake/bcm/wiki/Cmake-best-practices-and-guidelines). So, the script file I propose is just for convenience and keeping your CMake file clean. – compor Nov 29 '19 at 18:08