Skip to content

Extend add_compile_options (and other similar) with conditions

In our project I'm trying to use add_compile_options (and other similar commands) with generator expressions. However, generator expressions quickly grow very verbose and unmaintainable when you actually try to use them.

Problem

As an example, let's consider a case where I want to add an option -Wexample to C and C++ compilations on GCC and Clang, provided we are in mode controlled by ${MODE} variable. We will get the following generator expression:

$<
  $<AND:
    $<BOOL:${MODE}>,
    $<OR:
      $<COMPILE_LANG_AND_ID:C,GNU>,
      $<COMPILE_LANG_AND_ID:C,Clang>,
      $<COMPILE_LANG_AND_ID:CXX,GNU>,
      $<COMPILE_LANG_AND_ID:CXX,Clang>
    >
  >
  :-Wexample
>

I had to break it into lines and use indentation to make it readable. Although with actual CMake that would not work unless I would also quote the entire thing.

However, the fun only starts here. Let's now extend further assuming that the -Wexample option is available only from GCC A.B and Clang C.D versions. The corresponding generator expression "explodes":

$<
  $<AND:
    $<BOOL:${MODE}>,
    $<OR:
      $<AND:$<COMPILE_LANG_AND_ID:C,GNU>,$<VERSION_GREATER_EQUAL:$<C_COMPILER_ID>,A.B>>,
      $<AND:$<COMPILE_LANG_AND_ID:C,Clang>,$<VERSION_GREATER_EQUAL:$<C_COMPILER_ID>,C.D>>,
      $<AND:$<COMPILE_LANG_AND_ID:CXX,GNU>,$<VERSION_GREATER_EQUAL:$<CXX_COMPILER_ID>,A.B>>,
      $<AND:$<COMPILE_LANG_AND_ID:CXX,Clang>,$<VERSION_GREATER_EQUAL:$<CXX_COMPILER_ID>,C.D>>
    >
  >
  :-Wexample
>

This can be simplified a bit by naming some expressions as CMake variables. For example:

set(IS_MODE "$<BOOL:${MODE}>")

set(IS_C_GNU "$<COMPILE_LANG_AND_ID:C,GNU>")
set(IS_CXX_GNU "$<COMPILE_LANG_AND_ID:CXX,GNU>")
set(IS_C_OR_CXX_GNU "$<OR:${IS_C_GNU},${IS_CXX_GNU}>")

set(IS_C_CLANG "$<COMPILE_LANG_AND_ID:C,Clang>")
set(IS_CXX_CLANG "$<COMPILE_LANG_AND_ID:CXX,Clang>")
set(IS_C_OR_CXX_CLANG "$<OR:${IS_C_CLANG},${IS_CXX_CLANG}>")

set(IS_C_GNU_OR_CLANG "$<OR:${IS_C_GNU},${IS_C_CLANG}>")
set(IS_CXX_GNU_OR_CLANG "$<OR:${IS_CXX_GNU},${IS_CXX_CLANG}>")
set(IS_C_OR_CXX_GNU_OR_CLANG "$<OR:${IS_C_GNU_OR_CLANG},${IS_CXX_GNU_OR_CLANG}>")

With those in place, the first example reduces to:

$<$<AND:${IS_MODE},${IS_C_OR_CXX_GNU_OR_CLANG}>:-Wexample>

which isn't great but bearable I guess.

However, if we would like to include version checks as well, things get significantly worse as now we have to add also:

set(IS_C_GE_A_B "$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,A.B>")
set(IS_CXX_GE_A_B "$<VERSION_GREATER_EQUAL:$<CXX_COMPILER_VERSION>,A.B>")

set(IS_C_GE_C_D "$<VERSION_GREATER_EQUAL:$<C_COMPILER_VERSION>,C.D>")
set(IS_CXX_GE_C_D "$<VERSION_GREATER_EQUAL:$<CXX_COMPILER_VERSION>,C.D>")

set(IS_C_GNU_GE_A_B "$<AND:${IS_C_GNU},${IS_C_GE_A_B}>")
set(IS_CXX_GNU_GE_A_B "$<AND:${IS_CXX_GNU},${IS_CXX_GE_A_B}>")
set(IS_C_OR_CXX_GNU_GE_A_B "$<OR:${IS_C_GNU_GE_A_B},${IS_CXX_GNU_GE_A_B}>")

set(IS_C_CLANG_GE_C_D "$<AND:${IS_C_CLANG},${IS_C_GE_C_D}>")
set(IS_CXX_CLANG_GE_C_D "$<AND:${IS_CXX_CLANG},${IS_CXX_GE_C_D}>")
set(IS_C_OR_CXX_CLANG_GE_C_D "$<OR:${IS_C_CLANG_GE_C_D},${IS_CXX_CLANG_GE_C_D}>")

while the final expression from the example becomes:

$<$<AND:${IS_MODE},$<OR:${IS_C_OR_CXX_GNU_GE_A_B},${IS_C_OR_CXX_CLANG_GE_C_D}>>:-Wexample>

Proposed Solution

For our project, I have made a custom command add_compile_options_conditionally. I'm still adjusting the "keywords" and their working based on cases covered, but my current idea on this is something like:

add_compile_options_conditionally(
  <options>...
  [WHEN_CONDITIONS <positive_conditions>...]
  [WHEN_NOT_CONDITIONS <negative_conditions>...]
  [WHEN_LANGUAGUES <languages>...]
  [WHEN_COMPILER_IDS <compiler_ids>...]
)

This function calls add_compile_options underneath for each <options>, wrapping it in a generator expression implied by provided options. Those "implied" generator expressions are:

  1. If we have both WHEN_COMPILER_IDS and WHEN_LANGUAGUES a condition
    $<OR:$<COMPILE_LANG_AND_ID:{language},{compiler_id}>,...>
    is made as a cartesian product of <languages> and <compiler_ids>.
  2. If we have WHEN_COMPILER_IDS but not WHEN_LANGUAGUES, the same is done as above but now instead we assume all languages supported by CMake.
  3. If we have WHEN_LANGUAGUES but not WHEN_COMPILER_IDS a condition
    $<OR:$<COMPILE_LANGUAGE:{language}>,...>
    is made. I don't think this is useful, but it at least fitted well "algorithmically".
  4. If we have WHEN_CONDITIONS a condition
    $<OR:$<BOOL:{positive_condition}>,...>
    is made.
  5. If we have WHEN_NOT_CONDITIONS a condition
    $<OR:$<NOT:$<BOOL:{positive_condition}>>,...>
    is made.
  6. If more than one above point hits, all conditions are joined by $<AND:...>.

The first example now reduces to:

add_compile_options_conditionally(
  -Wexample
  WHEN_CONDITIONS ${MODE}
  WHEN_LANGUAGES C CXX
  WHEN_COMPILER_IDS Clang GNU
)

which seems much better to me.

However, as you can see, I still haven't addressed the problem of the compiler version. I'm not sure what syntax to pick for it so that it will be nice to use and reasonable to implement. At the moment I'm considering two alternatives:

  1. pkg-config style, like:
    WHEN_COMPILERS Clang>=12.0 GCC<9.0
    However, I'm afraid all the parsing will be on me and this doesn't seem simple enough.
  2. More structure, like:
    WHEN_COMPILERS Clang FROM 12.0 GCC UP_TO 9.0
    I expect the cmake_parse_arguments should be able to do that with a nested call. However, I'm afraid with this only one compiler will be allowed and it seems too verbose.

I would like to see hear some advice or perhaps better ideas.

Also, I'm still not sure:

  1. How well would this fit with the target_compile_options and its structure of arguments?
  2. How will de-duplication of arguments work in this new setting.

Finally

Summarizing:

  1. Could we add add_compile_options_conditionally to the language? Or at least as a module.
  2. Could we have a standard variable listing all supported languages?
Edited by Adam Badura
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information