CMake grouping/combining -include directives/flags

Hi!

I’m trying to specify multiple -include blah includes via generator expressions. CMake is not generating the expected compilation command, i.e.

-include cstddef -include csignal

but instead

-include cstddef csignal

which GCC doesn’t accept.

Here’s an example CMake config that has this problem:

 set(CODE_NAME ToyRts)
 set(CODE_VERSION 0.1.0)

 cmake_minimum_required(VERSION 3.12.0)

 project(${CODE_NAME} VERSION ${CODE_VERSION} LANGUAGES CXX)

 set(CMAKE_CXX_STANDARD 17)

 set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

 add_executable(
   Main
   Main.cpp
 )

 set_property(TARGET Main
   APPEND PROPERTY
   COMPILE_OPTIONS
   $<$<COMPILE_LANGUAGE:CXX>:-include csignal>)

 set_property(TARGET Main
   APPEND PROPERTY
   COMPILE_OPTIONS
   $<$<COMPILE_LANGUAGE:CXX>:-include cstddef>)

Any Main.cpp will produce the issue. CMake generates the following compile command:

/usr/bin/c++ -include csignal cstddef -flto=auto -ffat-lto-objects -std=gnu++17 -o CMakeFiles/Main.dir/Main.cpp.o -c /home/nils/SpECTRE/ToyRts/Main.cpp

and then GCC complains with:

c++: warning: cstddef: linker input file unused because linking not done
c++: error: cstddef: linker input file not found: No such file or directory

as it should since somehow the incorrect compile command was generated. If I build with

/usr/bin/c++ -include csignal -include cstddef -flto=auto -ffat-lto-objects -std=gnu++17 -o CMakeFiles/Main.dir/Main.cpp.o -c /home/nils/SpECTRE/ToyRts/Main.cpp

everything compiles fine. This is the compilation command I would expect CMake to generate.

Any suggestions on how to get CMake to not combine the two -include directives would be appreciated!

Best wishes,

Nils

CMake uniquifies flags. It also has no abstraction for forced inclusion. Why can you not just add the appropriate #include lines to the files in question?

Let’s get some related things out of the way first:

  • You should quote your generator expressions when they contain spaces or semicolons. See the following article for a detailed explanation of why (especially the Generator Expressions section): https://crascit.com/2022/01/25/quoting-in-cmake/
  • Prefer to use target_compile_options() rather than manipulating the COMPILE_OPTIONS target property directly.

Now to the core of your question. The COMPILE_OPTIONS target property is subject to compiler option de-duplication. Because you haven’t put quotes around your generator expression, CMake actually sees your generator expression as two separate arguments to the set_property() command. This results in a semicolon being used to separate the -include and csignal parts of what you tried to set. That in turn makes -include and csignal separately subject to de-duplication rather than the whole expression. Since -include will appear multiple times, CMake de-duplicates that and all but the first -include gets removed.

The SHELL: prefix is designed to handle this situation. It allows you to specify multiple options in a way that keeps them together. See the “Option De-duplication” section of the target_compile_options() docs. I think you’ll get the behavior you want if you replace your two set_property() calls with the following:

target_compile_options(Main PRIVATE
    "$<$<COMPILE_LANGUAGE:CXX>:SHELL:-include csignal>"
    "$<$<COMPILE_LANGUAGE:CXX>:SHELL:-include cstddef>"
)

But like @ben.boeckel said, your source code should use #include rather than relying on -include compiler flags for something like this.

Hi Ben and Craig!

Thanks for the very helpful responses!

The actual use-case is a bit more complicated than the example. The code base uses a PCH, which GCC needs to have specified as -include. Additionally, in debug mode we override the error handling of a 3rd party library (Blaze) with a macro definition (this is the official way to change error handling behavior in Blaze). The goal is to raise a SIGTRAP before throwing an exception (catch throw in GDB isn’t super user-friendly in our use-case). This means we’ve effectively inserted a new include requirement into the 3rd-party header-only library. While we could try to maintain wrapper headers for every interface header that Blaze provides, this is a bit tedious and adds non-negligible overhead. Instead, having the Blaze CMake target specify -include csignal seems more maintainable. I don’t love it, so if you have alternative suggestions they would be appreciated!

I’ve forgotten why this CMake code ended up using set_property instead of target_compile_options, but I’ll see if our CI passes with target_compile_options instead. Looking at the Cake 3.12 documentation (the oldest we support), there doesn’t seem to be any reason to not use target_compile_options.

CI will have the final say in terms of any edge cases with compiler settings, but this seems to be working correctly in the 3 build variations I’ve tried locally. Thank you both so much! I and the rest of the team really appreciate it!

Best,

Nils

Why would target_compile_definitions(Blaze INTERFACE "$<$<CONFIG:Debug>:SYMBOL_YOU_NEED>") not be sufficient? Arguably, <csignal> can be included in Blaze itself if that is set.

Hi Ben!

Thanks for the suggestion! I’m not sure I completely follow, so my apologies if have misinterpreted. The definition we add is:

   target_compile_options(Blaze
     INTERFACE
     "$<$<COMPILE_LANGUAGE:CXX>:SHELL:
     -D 'BLAZE_THROW(EXCEPTION)=struct sigaction handler{}\;handler.sa_handler=\
 SIG_IGN\;handler.sa_flags=0\;sigemptyset(&handler.sa_mask)\;\
 sigaction(SIGTRAP,&handler,nullptr)\;raise(SIGTRAP)\;throw EXCEPTION'
     >")

The problem is that SIGTRAP is a preprocessor macro defined in <csignal> so I can’t forward declare that.

I think your second suggestion is that Blaze could include csignal for us. While true, since they don’t need it unless someone changes the error handling in this particular way, I’m not sure I’ll have a good argument for them to include the header in a “just in case” way.

Best,

Nils

I think it’d be better to have Blaze do something like:

#if defined(BLAZE_OVERRIDE_THROW_HEADER)
#include BLAZE_OVERRIDE_THROW_HEADER
#endif

You can then stuff whatever you want in a real header and tell Blaze how to include it via target_compile_definitions.