[CMake] Include source files on a per-configuration basis

Michael Hertling mhertling at online.de
Fri Oct 21 10:24:06 EDT 2011


On 10/06/2011 10:29 AM, Michael Wild wrote:
> On 10/06/2011 08:14 AM, Michael Hertling wrote:
>> On 10/06/2011 07:04 AM, Michael Wild wrote:
>>> On Thu 06 Oct 2011 05:17:00 AM CEST, Michael Hertling wrote:
>>>> On 10/05/2011 10:47 PM, Robert Dailey wrote:
>>>>> In my particular CMake project, I have three CPP files:
>>>>>
>>>>>     a.cpp
>>>>>     b.cpp
>>>>>     c.cpp
>>>>>
>>>>> I want 'a.cpp' to be compiled in all configurations (release & debug).<br>
>>>>> I only want 'b.cpp' to be compiled in DEBUG configuration.<br>
>>>>> I only want 'c.cpp' to be compiled in RELEASE configuration.
>>>>>
>>>>> How can I do this? I need something similar to the `debug` and `optimized`
>>>>> keywords that are accepted by the `target_link_libraries()` CMake operation.
>>>>
>>>> If it's okay that b.cpp and c.cpp are compiled in all configurations but
>>>> incorporated in the final binaries only in the DEBUG or in the RELEASE
>>>> configuration, respectively, you might do the following:
>>>>
>>>> CMAKE_MINIMUM_REQUIRED(VERSION 2.8 FATAL_ERROR)
>>>> PROJECT(IMPORTEDEMPTY C)
>>>> SET(CMAKE_VERBOSE_MAKEFILE ON)
>>>> # Add library for DEBUG:
>>>> FILE(WRITE ${CMAKE_BINARY_DIR}/b.c "void b(void){}\n")
>>>> ADD_LIBRARY(b STATIC b.c)
>>>> # Add library for RELEASE:
>>>> FILE(WRITE ${CMAKE_BINARY_DIR}/c.c "void c(void){}\n")
>>>> ADD_LIBRARY(c STATIC c.c)
>>>> # Add empty static library:
>>>> FILE(WRITE ${CMAKE_BINARY_DIR}/empty.c "")
>>>> ADD_LIBRARY(empty STATIC empty.c)
>>>> # Reimport empty static library:
>>>> EXPORT(TARGETS empty NAMESPACE imported FILE importedempty.cmake)
>>>> INCLUDE(${CMAKE_BINARY_DIR}/importedempty.cmake)
>>>> # Impose IMPORTED_LINK_INTERFACE_LIBRARIES_{DEBUG,RELEASE} properties:
>>>> FOREACH(i IN LISTS CMAKE_CONFIGURATION_TYPES ITEMS ${CMAKE_BUILD_TYPE})
>>>>     STRING(TOUPPER "${i}" i)
>>>>     IF(i STREQUAL "DEBUG")
>>>>         SET_TARGET_PROPERTIES(importedempty PROPERTIES
>>>>             IMPORTED_LINK_INTERFACE_LIBRARIES_${i} b)
>>>>     ELSEIF(i STREQUAL "RELEASE")
>>>>         SET_TARGET_PROPERTIES(importedempty PROPERTIES
>>>>             IMPORTED_LINK_INTERFACE_LIBRARIES_${i} c)
>>>>     ENDIF()
>>>> ENDFOREACH()
>>>> # Specify required dependencies:
>>>> ADD_DEPENDENCIES(importedempty empty b c)
>>>> # Add final binary:
>>>> FILE(WRITE ${CMAKE_BINARY_DIR}/a.c "int main(void){return 0;}\n")
>>>> ADD_EXECUTABLE(a a.c)
>>>> TARGET_LINK_LIBRARIES(a importedempty)
>>>>
>>>> Adventurous, but somewhat clean; see [1] for an explanation, and be
>>>> especially careful with a file named "libc.a" on *nix systems. ;-)
>>>>
>>>> If you really need to avoid the compilation of b.cpp or c.cpp in
>>>> certain configurations, you might try the following approach:
>>>>
>>>> CMAKE_MINIMUM_REQUIRED(VERSION 2.8 FATAL_ERROR)
>>>> PROJECT(RECONF C)
>>>> SET(CMAKE_VERBOSE_MAKEFILE ON)
>>>> FILE(WRITE ${CMAKE_BINARY_DIR}/a.c "int main(void){return 0;}\n")
>>>> FILE(WRITE ${CMAKE_BINARY_DIR}/b.c "void b(void){}\n")
>>>> FILE(WRITE ${CMAKE_BINARY_DIR}/c.c "void c(void){}\n")
>>>> STRING(TOUPPER "${CONF}" CONF)
>>>> IF(CONF STREQUAL "DEBUG")
>>>>     ADD_EXECUTABLE(a0 EXCLUDE_FROM_ALL a.c b.c)
>>>> ELSEIF(CONF STREQUAL "RELEASE")
>>>>     ADD_EXECUTABLE(a0 EXCLUDE_FROM_ALL a.c c.c)
>>>> ELSE()
>>>>     ADD_EXECUTABLE(a0 EXCLUDE_FROM_ALL a.c)
>>>> ENDIF()
>>>> ADD_CUSTOM_TARGET(a ALL
>>>>     COMMAND ${CMAKE_COMMAND}
>>>>         -DCONF="$<CONFIGURATION>"
>>>>         ${CMAKE_BINARY_DIR}
>>>>     COMMAND ${CMAKE_COMMAND}
>>>>         --build ${CMAKE_BINARY_DIR}
>>>>         --config "$<CONFIGURATION>"
>>>>         --target a0)
>>>>
>>>> Effectively, when target "a" is built, the project reconfigures itself
>>>> with the current configuration passed in via "CONF" and with a helper
>>>> target "a0" which is made up from the configuration-specific sources;
>>>> finally, this target "a0" is built with the current configuration.
>>>> This can be seen working on *nix with Makefiles, but there might
>>>> be issues with other generators and IDEs.
>>>>
>>>> 'hope that helps.
>>>>
>>>> Regards,
>>>>
>>>> Michael
>>>>
>>>> [1] http://www.mail-archive.com/cmake@cmake.org/msg34680.html
>>>
>>> I think it would be much easier to have a wrapper file, say b_or_c.cpp
>>> which #include's b.cpp or c.cpp at compile time depending on the current
>>> configuration. E.g. like this:
>>>
>>> ///////////////////////////////////////////////////////////
>>> #if defined USE_B_CPP
>>> #  include "b.cpp"
>>> #elseif defined USE_C_CPP
>>> #  include "c.cpp"
>>> #else // what should happen otherwise?
>>> #  error Either USE_B_CPP or USE_C_CPP must be defined!
>>> #endif
>>> ///////////////////////////////////////////////////////////
>>>
>>>
>>> And then in your CMakeLists.txt you do:
>>>
>>> ###########################################################
>>> set_source_files_properties(b_or_c.cpp PROPERTIES
>>>   COMPILE_DEFINITIONS_DEBUG USE_B_CPP
>>>   COMPILE_DEFINITIONS_RELEASE USE_C_CPP
>>>   # what should happen in a default build?
>>>   # Or RELWITHDEBINFO and MINSIZEREL?
>>>   )
>>> ###########################################################
>>
>> Yes, this would work, too, but if neither b.cpp nor c.cpp should be
>> compiled if the current configuration is neither DEBUG nor RELEASE,
>> the b_or_c.cpp file would be effectively empty, and adding an object
>> file compiled from an empty source file to a binary is not 100 % the
>> same as dropping the object file completely - at least with gcc and
>> even with -Os. However, it's a quite negligible effect, but linking
>> against an empty static library or building a reconfigured project
>> means *exactly* the same as if b.cpp and c.cpp have been left out.
>>
>> Regards,
>>
>> Michael
> 
> You still could compile b_or_c.cpp into a static library first, no
> trickery with IMPORTED required. However, if that static library is
> going to be linked to a shared library, you would need to make sure that
> CMAKE_SHARED_LIBRARY_CXX_FLAGS is included in the COMPILE_FLAGS of the
> static library.

Yes, in simple cases, this would work fine, but if there're further
dependencies, it probably won't yield the desired results. Suppose
b.c and c.c refer to d() in libd.so, so libb_or_c.a is formally
linked against the latter by TARGET_LINK_LIBRARIES(b_or_c d):

CMAKE_MINIMUM_REQUIRED(VERSION 2.8 FATAL_ERROR)
PROJECT(B_OR_C C)
SET(CMAKE_VERBOSE_MAKEFILE ON)
FILE(WRITE ${CMAKE_BINARY_DIR}/d.c "void d(void){}\n")
ADD_LIBRARY(d SHARED d.c)
FILE(WRITE ${CMAKE_BINARY_DIR}/b.c "void b(void){d();}\n")
FILE(WRITE ${CMAKE_BINARY_DIR}/c.c "void c(void){d();}\n")
FILE(WRITE ${CMAKE_BINARY_DIR}/b_or_c.c
"#if defined USE_B
#include \"b.c\"
#elif defined USE_C
#include \"c.c\"
#endif\n")
ADD_LIBRARY(b_or_c STATIC b_or_c.c)
TARGET_LINK_LIBRARIES(b_or_c d)
FILE(WRITE ${CMAKE_BINARY_DIR}/main.c
"int main(void){
#if defined USE_B
b();
#elif defined USE_C
c();
#endif
return 0;}\n")
ADD_EXECUTABLE(main main.c)
TARGET_LINK_LIBRARIES(main b_or_c)
SET_SOURCE_FILES_PROPERTIES(b_or_c.c main.c
    PROPERTIES
    COMPILE_DEFINITIONS_DEBUG USE_B
    COMPILE_DEFINITIONS_RELEASE USE_C)

If configured with -DCMAKE_BUILD_TYPE={debug,release}, everything
works as expected, but with -UCMAKE_BUILD_TYPE, the main target is
still linked against libb_or_c.a's prerequisite libd.so which might
be undesirable. AFAIK, there is currently no possibility to prevent
an ordinary static library's prerequisites from being referred to as
the LINK_INTERFACE_LIBRARIES properties are ignored for this type of
targets. However, they are considered for imported static libraries:

CMAKE_MINIMUM_REQUIRED(VERSION 2.8 FATAL_ERROR)
PROJECT(B_OR_C C)
SET(CMAKE_VERBOSE_MAKEFILE ON)
FILE(WRITE ${CMAKE_BINARY_DIR}/d.c "void d(void){}\n")
ADD_LIBRARY(d SHARED d.c)
FILE(WRITE ${CMAKE_BINARY_DIR}/b.c "void b(void){d();}\n")
FILE(WRITE ${CMAKE_BINARY_DIR}/c.c "void c(void){d();}\n")
FILE(WRITE ${CMAKE_BINARY_DIR}/b_or_c.c
"#if defined USE_B
#include \"b.c\"
#elif defined USE_C
#include \"c.c\"
#endif\n")
ADD_LIBRARY(b_or_c STATIC b_or_c.c)
TARGET_LINK_LIBRARIES(b_or_c d)
# -->
FILE(WRITE ${CMAKE_BINARY_DIR}/empty.c "")
ADD_LIBRARY(empty STATIC empty.c)
EXPORT(TARGETS empty NAMESPACE imported FILE importedempty.cmake)
INCLUDE(${CMAKE_BINARY_DIR}/importedempty.cmake)
SET_TARGET_PROPERTIES(importedempty PROPERTIES
    IMPORTED_LINK_INTERFACE_LIBRARIES_DEBUG b_or_c
    IMPORTED_LINK_INTERFACE_LIBRARIES_RELEASE b_or_c)
ADD_DEPENDENCIES(importedempty empty b_or_c)
# <--
FILE(WRITE ${CMAKE_BINARY_DIR}/main.c
"int main(void){
#if defined USE_B
b();
#elif defined USE_C
c();
#endif
return 0;}\n")
ADD_EXECUTABLE(main main.c)
TARGET_LINK_LIBRARIES(main importedempty)
SET_SOURCE_FILES_PROPERTIES(b_or_c.c main.c
    PROPERTIES
    COMPILE_DEFINITIONS_DEBUG USE_B
    COMPILE_DEFINITIONS_RELEASE USE_C)

With this reimported-empty-static-library approach, libb_or_c.a's
prerequisite libd.so is omitted in the main target's link command
line if CMAKE_BUILD_TYPE isn't in {debug,release}. This is because
the IMPORTED_LINK_INTERFACE_LIBRARIES[_<CONFIG>] target properties
allow for cutting the conditional library's entire dependency chain
in a configuration-specific manner. IMO, the seven/eight additional
lines are not sooo bad and yield really clean results. Admittedly,
in simple cases, this approach might be taken for exaggerated.

> OTOH, putting the content of b_or_c.cpp into a file that is
> unconditionally compiled would solve the whole problem in one go... That
> file would naturally be the client code of what is defined in b.cpp or
> c.cpp.

A quite appropriate solution I could imagine for the OP's concern is
a source file property EXCLUDE_FROM_<CONFIG> - similar as requested
in issues #6314,11902 - or a target property EXTRA_SOURCES_<CONFIG>.
Conceptually, both should be easy to implement for Makefiles and go
well with VS's ExcludeFromBuild property. Does anybody know whether
and, as the case may be, how other IDEs support a feature like that?

Regards,

Michael


More information about the CMake mailing list