CMake - remove a compile flag for a single translation unit

I would like to remove a set compile flag for a single translation unit. Is there a way to do this? (e.g. using set_property?)

Note: the compile-flag has no -fno-name negation (for whatever reason).

I've tried:

get_property(FLAGS TARGET target PROPERTY COMPILE_FLAGS)
string(REPLACE "-fname" "" FLAGS ${FLAGS})
set_property(TARGET target PROPERTY COMPILE_FLAGS ${FLAGS})

without any luck. The property I want to remove is part of CMAKE_CXX_FLAGS and thus this does not work.


Solution 1:

This particular solution(?) will work both for targets and for single translation units (a single .cpp-file). It will require CMake 3.7 or newer, since we need to iterate through all existing targets using the BUILDSYSTEM_TARGETS property, which was added in 3.7. If you are unable to use 3.7 or newer, you can adapt apply_global_cxx_flags_to_all_targets() to take a list of targets and specify them manually. Note that you should consider the macros below as proof-of-concept. They will probably need some tweaking and improvement for any but very small projects.

The first step is to iterate through all existing targets and apply CMAKE_CXX_FLAGS to each of them. When this is done, we clear CMAKE_CXX_FLAGS. Below you'll find a macro that will do this. This macro should probably be called somewhere at the bottom of your top-level CMakeLists.txt, when all targets have been created, all flags have been set etc. It's important to note that the command get_property(_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) works at directory level, so if you use add_subdirectory(), you must call this macro in the top-level CMakeLists.txt of that sub-directory.

#
# Applies CMAKE_CXX_FLAGS to all targets in the current CMake directory.
# After this operation, CMAKE_CXX_FLAGS is cleared.
#
macro(apply_global_cxx_flags_to_all_targets)
    separate_arguments(_global_cxx_flags_list UNIX_COMMAND ${CMAKE_CXX_FLAGS})
    get_property(_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
    foreach(_target ${_targets})
        target_compile_options(${_target} PUBLIC ${_global_cxx_flags_list})
    endforeach()
    unset(CMAKE_CXX_FLAGS)
    set(_flag_sync_required TRUE)
endmacro()

This macro first creates a list from CMAKE_CXX_FLAGS, then it gets a list of all targets and applies CMAKE_CXX_FLAGS to each of the targets. Finally, CMAKE_CXX_FLAGS is cleared. _flag_sync_required is used to indicate if we need to force a rewrite of cached variables.

The next step depends if you want to remove a flag from a target or from a particular translation unit. If you want to remove a flag from a target, you can use a macro similar to this:

#
# Removes the specified compile flag from the specified target.
#   _target     - The target to remove the compile flag from
#   _flag       - The compile flag to remove
#
# Pre: apply_global_cxx_flags_to_all_targets() must be invoked.
#
macro(remove_flag_from_target _target _flag)
    get_target_property(_target_cxx_flags ${_target} COMPILE_OPTIONS)
    if(_target_cxx_flags)
        list(REMOVE_ITEM _target_cxx_flags ${_flag})
        set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_target_cxx_flags}")
    endif()
endmacro()

Removing a flag from a particular translation unit is a bit trickier (unless I have greatly missed something). Anyway, the idea is to first obtain the compile options from the target to which the file belongs, and then applying said options to all source files in that target, which allows us to manipulate the compile flags for individual files. We do this by maintaining a cached list of compile flags for each file we want to remove flags from, and when a remove is requested, we remove it from the cached list and then re-apply the remaining flags. The compile options for the target itself is cleared.

#
# Removes the specified compiler flag from the specified file.
#   _target     - The target that _file belongs to
#   _file       - The file to remove the compiler flag from
#   _flag       - The compiler flag to remove.
#
# Pre: apply_global_cxx_flags_to_all_targets() must be invoked.
#
macro(remove_flag_from_file _target _file _flag)
    get_target_property(_target_sources ${_target} SOURCES)
    # Check if a sync is required, in which case we'll force a rewrite of the cache variables.
    if(_flag_sync_required)
        unset(_cached_${_target}_cxx_flags CACHE)
        unset(_cached_${_target}_${_file}_cxx_flags CACHE)
    endif()
    get_target_property(_${_target}_cxx_flags ${_target} COMPILE_OPTIONS)
    # On first entry, cache the target compile flags and apply them to each source file
    # in the target.
    if(NOT _cached_${_target}_cxx_flags)
        # Obtain and cache the target compiler options, then clear them.
        get_target_property(_target_cxx_flags ${_target} COMPILE_OPTIONS)
        set(_cached_${_target}_cxx_flags "${_target_cxx_flags}" CACHE INTERNAL "")
        set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "")
        # Apply the target compile flags to each source file.
        foreach(_source_file ${_target_sources})
            # Check for pre-existing flags set by set_source_files_properties().
            get_source_file_property(_source_file_cxx_flags ${_source_file} COMPILE_FLAGS)
            if(_source_file_cxx_flags)
                separate_arguments(_source_file_cxx_flags UNIX_COMMAND ${_source_file_cxx_flags})
                list(APPEND _source_file_cxx_flags "${_target_cxx_flags}")
            else()
                set(_source_file_cxx_flags "${_target_cxx_flags}")
            endif()
            # Apply the compile flags to the current source file.
            string(REPLACE ";" " " _source_file_cxx_flags_string "${_source_file_cxx_flags}")
            set_source_files_properties(${_source_file} PROPERTIES COMPILE_FLAGS "${_source_file_cxx_flags_string}")
        endforeach()
    endif()
    list(FIND _target_sources ${_file} _file_found_at)
    if(_file_found_at GREATER -1)
        if(NOT _cached_${_target}_${_file}_cxx_flags)
            # Cache the compile flags for the specified file.
            # This is the list that we'll be removing flags from.
            get_source_file_property(_source_file_cxx_flags ${_file} COMPILE_FLAGS)
            separate_arguments(_source_file_cxx_flags UNIX_COMMAND ${_source_file_cxx_flags})
            set(_cached_${_target}_${_file}_cxx_flags ${_source_file_cxx_flags} CACHE INTERNAL "")
        endif()
        # Remove the specified flag, then re-apply the rest.
        list(REMOVE_ITEM _cached_${_target}_${_file}_cxx_flags ${_flag})
        string(REPLACE ";" " " _cached_${_target}_${_file}_cxx_flags_string "${_cached_${_target}_${_file}_cxx_flags}")
        set_source_files_properties(${_file} PROPERTIES COMPILE_FLAGS "${_cached_${_target}_${_file}_cxx_flags_string}")
    endif()
endmacro()

Example

We have this very simple project structure:

source/
    CMakeLists.txt
    foo.cpp
    bar.cpp
    main.cpp

Let's assume that foo.cpp violates -Wunused-variable, and we want to disable that flag on that particular file. For bar.cpp, we want to disable -Werror. For main.cpp, we want to remove -O2, for whatever reason.

CMakeLists.txt

cmake_minimum_required(VERSION 3.7 FATAL_ERROR)
project(MyProject)

# Macros omitted to save space.

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -Wunused-variable")
# CMake will apply this to all targets, so this will work too.
add_compile_options("-Werror")

add_executable(MyTarget foo.cpp bar.cpp main.cpp)

apply_global_cxx_flags_to_all_targets()
remove_flag_from_file(MyTarget foo.cpp -Wunused-variable)
remove_flag_from_file(MyTarget bar.cpp -Werror)
remove_flag_from_file(MyTarget main.cpp -O2)

If you instead want to remove a flag from a target, you would use remove_flag_from_target(), for example: remove_flag_from_target(MyTarget -O2)

Result

Output before applying macros

clang++  -Werror -O2 -Wunused-variable -o foo.cpp.o -c foo.cpp
clang++  -Werror -O2 -Wunused-variable -o bar.cpp.o -c bar.cpp
clang++  -Werror -O2 -Wunused-variable -o main.cpp.o -c main.cpp

Output after applying macros

clang++  -Werror -O2  -o foo.cpp.o -c foo.cpp
clang++  -O2 -Wunused-variable  -o bar.cpp.o -c bar.cpp
clang++  -Werror -Wunused-variable -o main.cpp.o -c main.cpp

Final notes

As mentioned, this should be seen as a proof-of-concept. There a few immediate issues to consider:

  • It only considers CMAKE_CXX_FLAGS (common to all build types), not CMAKE_CXX_FLAGS_<DEBUG|RELEASE> etc.
  • The macros can only handle one flag at a time
  • remove_flag_from_file() can only handle one target and input file as input at a time.
  • remove_flag_from_file() expects a filename, not a path. Passing, say, source/foobar/foobar.cpp will not work, since source/foobar/foobar.cpp will be compared against foobar.cpp. This can be fixed by using get_source_file_property() and the LOCATION property and placing a precondition that _file is a full path.
    • What happens if a target have two or more files with the same name?
  • remove_flag_from_file() can probably be optimized and improved greatly.
  • The call to separate_arguments() assumes Unix.

Most of these should be fairly easy to fix.

I hope that this will at least nudge you in the right direction in solving this problem. Let me know if I need to add something to the answer.

Solution 2:

If there is no compile flag negation option (which would be the standard way of CMake handling this for non-IDE configurators), you have to remove this compile flag from CMAKE_CXX_FLAGS.

Possible Approaches

  1. Make COMPILE_FLAGS a source file property which is INHERITED from a directory property with the same name. Now you can modify COMPILE_FLAGS property of each directory and source file individually:

    # Remove '-fname' from global flags
    string(REPLACE "-fname" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
    
    define_property(
        SOURCE
        PROPERTY COMPILE_FLAGS 
        INHERITED 
        BRIEF_DOCS "brief-doc"
        FULL_DOCS  "full-doc"
    )
    
    # Add '-fname' for directory
    set_directory_properties(PROPERTIES COMPILE_FLAGS "-fname")
    
    add_executable(${PROJECT_NAME} "main.cpp")
    
    # Remove '-fname' from source file
    set_source_files_properties("main.cpp" PROPERTIES COMPILE_FLAGS "")
    
  2. If some property - like COMPILE_OPTIONS - is only available on target level, you have to move the files you want to have different settings for into a separate new target itself.

    This can be done by using OBJECT libraries:

    # Remove '-fname' from global flags
    string(REPLACE "-fname" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
    
    # Set '-fname' for directory, but if you also use add_compile_options() be aware that
    # this will overwrite your previous setting. Append/remove properties instead.
    set_directory_properties(
        PROPERTIES 
            COMPILE_OPTIONS "-fname"
    )
    
    # Remove '-fname' from source file(s) in separate object library target
    add_library(${PROJECT_NAME}_no_name OBJECT "main.cpp")
    set_target_properties(
        ${PROJECT_NAME}_no_name
        PROPERTIES 
            COMPILE_OPTIONS ""
    )
    
    # Use object library target in e.g. executable target
    add_executable(${PROJECT_NAME} $<TARGET_OBJECTS:${PROJECT_NAME}_no_name>)
    

References

  • CMake per file optimizations
  • Override compile flags for single files
  • clang-tidy cmake exclude file from check