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:
- If we have both
WHEN_COMPILER_IDS
andWHEN_LANGUAGUES
a condition$<OR:$<COMPILE_LANG_AND_ID:{language},{compiler_id}>,...>
<languages>
and<compiler_ids>
. - If we have
WHEN_COMPILER_IDS
but notWHEN_LANGUAGUES
, the same is done as above but now instead we assume all languages supported by CMake. - If we have
WHEN_LANGUAGUES
but notWHEN_COMPILER_IDS
a condition$<OR:$<COMPILE_LANGUAGE:{language}>,...>
- If we have
WHEN_CONDITIONS
a condition$<OR:$<BOOL:{positive_condition}>,...>
- If we have
WHEN_NOT_CONDITIONS
a condition$<OR:$<NOT:$<BOOL:{positive_condition}>>,...>
- 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:
-
pkg-config
style, like:WHEN_COMPILERS Clang>=12.0 GCC<9.0
- More structure, like:
WHEN_COMPILERS Clang FROM 12.0 GCC UP_TO 9.0
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:
- How well would this fit with the
target_compile_options
and its structure of arguments? - How will de-duplication of arguments work in this new setting.
Finally
Summarizing:
- Could we add
add_compile_options_conditionally
to the language? Or at least as a module. - Could we have a standard variable listing all supported languages?