Search code examples
cmakecompiler-flagsbuild-system

Correct way to handle compiler flags when using CMake


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

Solution

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