cmake

How can I overwrite interface compile options coming from a linked library in CMake?


I am running into a problem where my target has to link to a third party imported library that propagates a few unwanted compilation flags through INTERFACE_COMPILE_OPTIONS that I want to overwrite. Here's a minimal example to illustrate the problem:

project(sample)

add_library(third-party INTERFACE IMPORTED)
set_target_properties(third-party PROPERTIES INTERFACE_COMPILE_OPTIONS "-fno-rtti")

add_executable(sample main.cpp)
target_link_libraries(sample PUBLIC third-party)
target_compile_options(sample PUBLIC "-frtti")

In this example, the unwanted compilation flag that is being propagated by the third-party imported target is -fno-rtti. Unfortunately, adding target_compile_options(sample PUBLIC "-frtti") does not work because if I do make VERBOSE=1 to investigate the exact compilation command, for some reason, CMake decides to put my -frtti before the third-party library's -fno-rtti (and compilers tend to take the later when binary opposite flags contradict).

What is the correct way to overwrite compilation flags coming from a linked imported library in CMake? Is it a bug that CMake puts INTERFACE_COMPILE_OPTIONS from linked targets after its own COMPILE_OPTIONS thus giving priority to INTERFACE_COMPILE_OPTIONS from linked targets?

Note: For my real situation, the unwanted compilation flag actually came from dependencies layers down. E.g. modify the minimal example so that third-party has INTERFACE_LINK_LIBRARIES to Foo and Foo has INTERFACE_COMPILE_OPTIONS -fno-rtti instead of third-party.


Solution

  • My first question would be whether you should really be trying to be doing this. If a library specifies that a compile option is an interface compile option, then my first instinct is to trust them, and if I have doubts, to find out why- not to try to wrangle it away. Not that I'm doubting you, but if you haven't yet, you should try that kind of thinking.

    If you do that thinking and come to the conclusion that such a compile option shouldn't have been specified as being part of the INTERFACE of the target, then you should contact the maintainer of that CMake script for that depdendency and (humbly) explain why, requesting a change. If it's something that doesn't affect the generated target binary, such as a warning option, then they can just set it as a PRIVATE compile option on their target, or only add that compile option if their project is the top-level project (see PROJECT_IS_TOP_LEVEL and <PROJECT-NAME>_IS_TOP_LEVEL) (or some combination of those things).


    Note: This first section was written before the asker further clarified their context. I leave it because it is informative and may be useful to future readers.

    That being said, you could use list(REMOVE_ITEM) to solve this problem.

    Example usage:

    add_library(third-party INTERFACE) # can be IMPORTED. shouldn't matter
    set_target_properties(third-party PROPERTIES INTERFACE_COMPILE_OPTIONS "0;1;2;3;a;-fno-rtti;b;c;d")
    get_target_property(third-party_interface_compile_options third-party INTERFACE_COMPILE_OPTIONS)
    list(REMOVE_ITEM third-party_interface_compile_options "-fno-rtti")
    # message("third-party_interface_compile_options: ${third-party_interface_compile_options}")
    set_target_properties(third-party PROPERTIES INTERFACE_COMPILE_OPTIONS "${third-party_interface_compile_options}")
    

    Don't let the long lines of code fool you. There's nothing complicated going on. It gets the target property to a variable, modifies the variable, and then writes the modified value back to the target property. It's only long because of choice of long variable names. I prefer variable names to be clear rather than terse in certain kinds of scenarios- this being one of them.

    This will affect all targets that link with that target, whether directly or transitively. It could effect other targets in ways that you might not want. If that's the case, then read on to find out how to do the "override" for specific dependent targets instead of all of them.


    As for why CMake puts your "-frtti" before the target's interface "-fno-rtti", see the docs for the COMPILE_OPTIONS target property:

    The options will be added after flags in the CMAKE_<LANG>_FLAGS and CMAKE_<LANG>_FLAGS_<CONFIG> variables, but before those propagated from dependencies by the INTERFACE_COMPILE_OPTIONS property.

    Slightly related info: From a previous discussion with Craig Scott (one of the CMake maintainers), I learned that sometimes CMake uses such logic to favour certain prioritizations of include directory flags. It happens that compilers tend to search include directories from first to last (left to right), so the first one that gives a match for something is used, but they also tend to favour right-most / later flags when two flags are the binary opposite toggles of each other. As far as I know, CMake doesn't do anything special to handle that kind of nuance, and possibly made a decision in the past to favour certain logical prioritizations of include directory ordering.


    Okay, since you asked, if you really want to do something to fix this specific problem of yours without doing anything to touch the third party dependencies at whichever layer of transitive dependency it's at, I offer this hack:

    CMake does compile option de-duplications like so:

    The final set of options used for a target is constructed by accumulating options from the current target and the usage requirements of its dependencies. The set of options is de-duplicated to avoid repetition.

    From experimentation, the deduplication keeps the left-most / first copy of any duplicates.

    So to hack your solution, just do this:

    target_compile_options(sample PUBLIC "-fno-rtti;-frtti")
    

    Since you said your flags get put earlier than the interface flags, this will cause your flags to be the first copy of anything that duplicates. So the hacky inserted "-fno-rtti" will be kept instead of the third-party library's, and then the compiler will do its thing and keep the last toggle value of a binary flag, which will then be "-frtti".

    I see this as a hack, and I personally think such behaviour is surprising in a bad way. But it works to solve your problem here. Remember that discussion I mentioned I had with Craig Scott? I had it precisely because of this behaviour. link.


    Note: If you are a project maintainer and want to add an interface compile option to one of your targets while enabling dependent targets to choose not to inherit that interface compile option, use the following approach suggested by Ben Boeckel (one of the CMake maintainers)

    something like $<$<NOT:$<TARGET_PROPERTY:skip_this_flag>>:-fsome-flag> can be used so that consuming targets can set their own skip_this_flag property to ignore it.