3

I'm trying to persuade cmake to add a custom target which builds a precompiled header on Visual Studio (note: please do not suggest I used a custom build step instead, I specifically need a build target which builds a precompiled header). Note that you cannot simply add CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE} during the configuration stage as that won't work in generated .vcxproj's where the user can change the build configuration, so I need to somehow tell cmake in its build stage to output the right stuff for each possible configuration i.e. the appropriate configuration's CMAKE_CXX_FLAGS and CMAKE_CXX_FLAGS_$<CMAKE_BUILD_TYPE>

I'm very nearly there with this solution, except for one problem I'm asking for help with:

# Adds a custom target which generates a precompiled header
function(add_precompiled_header outvar headerpath)
  get_filename_component(header "${headerpath}" NAME)
  set(pchpath ${CMAKE_CURRENT_BINARY_DIR}/${header}.dir/${CMAKE_CFG_INTDIR}/${header}.pch)
  set(flags ${CMAKE_CXX_FLAGS})
  separate_arguments(flags)
  set(flags ${flags}
    $<$<CONFIG:Debug>:${CMAKE_CXX_FLAGS_DEBUG}>
    $<$<CONFIG:Release>:${CMAKE_CXX_FLAGS_RELEASE}>
    $<$<CONFIG:RelWithDebInfo>:${CMAKE_CXX_FLAGS_RELWITHDEBINFO}>
    $<$<CONFIG:MinSizeRel>:${CMAKE_CXX_FLAGS_MINSIZEREL}>
  )
  if(MSVC)
    add_custom_target(${outvar}
      COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/${header}.dir/${CMAKE_CFG_INTDIR}"
      COMMAND ${CMAKE_CXX_COMPILER} /c ${flags} /Fp"${pchpath}" /Yc"${header}" /Tp"${CMAKE_CURRENT_SOURCE_DIR}/${headerpath}"
      COMMENT "Precompiling header ${headerpath} ..."
      SOURCES "${headerpath}"
    )
  endif()
endfunction()

This very nearly works where the correct flags for each build configuration have been expanded into their respective command stanzas in the .vcxproj file:

<Command Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">setlocal
  "G:\Program Files\CMake\bin\cmake.exe" -E make_directory G:/boost.afio/cmake/afio.hpp.dir/$(Configuration)
  if %errorlevel% neq 0 goto :cmEnd
  "G:\Program Files\Microsoft Visual Studio 14.0\VC\bin\cl.exe" /c /DWIN32 /D_WINDOWS /W3 /GR /EHsc  "/MD /O2 /Ob2 /D NDEBUG"   /Fp"G:/boost.afio/cmake/afio.hpp.dir/$(Configuration)/afio.hpp.pch" /Yc"afio.hpp" /Tp"G:/boost.afio/include/boost/afio/afio.hpp"
  if %errorlevel% neq 0 goto :cmEnd
:cmEnd
  endlocal &amp; call :cmErrorLevel %errorlevel% &amp; goto :cmDone
:cmErrorLevel
  exit /b %1
:cmDone
  if %errorlevel% neq 0 goto :VCEnd
</Command>

The problem is that the generator $<$<CONFIG:Release>:${CMAKE_CXX_FLAGS_RELEASE}> due to containing spaces expands into the quoted string "/MD /O2 /Ob2 /D NDEBUG" and this of course causes the compiler to complain loudly.

What I need therefore is either one of:

  1. Some method of telling cmake generator expressions to not expand content containing a space into a quoted string.

OR

  1. Some other method of expanding CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE} into each of the configuration specific sections for an add_custom_target.

Many thanks in advance.

Edit: Based on Tsyvarev's answer below, I came up with this:

# Add generator expressions to appendvar expanding at build time any remaining parameters
# if the build configuration is config
function(expand_at_build_if_config config appendvar)
  set(ret ${${appendvar}})
  set(items ${ARGV})
  list(REMOVE_AT items 0 1)
  separate_arguments(items)
  foreach(item ${items})
    list(APPEND ret $<$<CONFIG:${config}>:${item}>)
  endforeach()
  set(${appendvar} ${ret} PARENT_SCOPE)
endfunction()

This is working well. Many thanks to Tsyvarev!

Edit 2: It turns out cmake has undocumented support for precompiled header generation at least for MSVC. Take a look at the add_precompiled_header() function in https://github.com/ned14/boost-lite/blob/master/cmake/BoostLitePrecompiledHeader.cmake, all you need to do is supply the /Yc flag to an OBJECT type library and voila, the Visual Studio generator does the right thing, correct .vcxproj XML stanzas and all.

Niall Douglas
  • 9,212
  • 2
  • 44
  • 54

2 Answers2

3

Split FLAGS by spaces, and for every flag append corresponded generator expression:

# Process Release flags.
set(FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE})
separate_arguments(FLAGS_RELEASE) # Flags are ready for iterate

foreach(FLAG_RELEASE ${FLAGS_RELEASE})
    list(APPEND flags $<$<CONFIG:Release>:${FLAG_RELEASE}>)
endforeach(FLAGS_RELEASE ${FLAGS_RELEASE})
# Flags for other build types are processed in the same way.
# ...

# Now 'flags' variable may be used for COMMAND
Tsyvarev
  • 60,011
  • 17
  • 110
  • 153
  • Wouldn't it be enough to just add `VERBATIM` to `add_custom_target()` once you have the parameters in a list? I don't think you have to iterate and add `$<$:...>` before each flag. See e.g. [here](http://stackoverflow.com/questions/35847655/cmake-when-to-quote-variables). – Florian Jul 07 '16 at 12:48
  • Don't know about Windows, but on Linux (Makefiles) CMake incorrectly expands generator expression if value is a list. *VERBATIM* doesn't help with that. – Tsyvarev Jul 07 '16 at 12:59
  • Yuck. But it might just work. I'll get back to you tomorrow with testing. – Niall Douglas Jul 07 '16 at 14:36
  • @NiallDouglas Just to follow-up on my comment: I've given your question's example with `VERBATIM` a try and it didn't make things better. – Florian Jul 07 '16 at 20:40
  • @Florian I'd already tried `VERBATIM` and indeed it doesn't help. Also that option adds backslashes before the double quotes surrounding the paths to the precompiled header files which is very unhelpful. – Niall Douglas Jul 08 '16 at 06:02
  • Confirmed, I just added a sample implementation based on your answer. Thank you! – Niall Douglas Jul 08 '16 at 07:08
1

I have given @Tsyvarev's answer a try and it does work just fine.

Generally speaking the issue you encountered is considered a bug or feature request in CMake itself. You may want to add your support to the still open CMake Issue #14353.

I just wanted to add an simple alternative using response files when generator expressions for compiler options don't work out:

file(
    WRITE "${CMAKE_CURRENT_BINARY_DIR}/Debug.flags" 
        "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_DEBUG}"
)
file(
    WRITE "${CMAKE_CURRENT_BINARY_DIR}/Release.flags" 
        "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_RELEASE}"
)
file(
    WRITE "${CMAKE_CURRENT_BINARY_DIR}/RelWithDebInfo.flags" 
        "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}"
)
file(
    WRITE "${CMAKE_CURRENT_BINARY_DIR}/MinSizeRel.flags" 
        "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_MINSIZEREL}"
)

# Now 'flags' files may be used for COMMAND
add_custom_target(
    ${outvar}
    COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/${header}.dir/${CMAKE_CFG_INTDIR}"
    COMMAND ${CMAKE_CXX_COMPILER} /c @$<CONFIG>.flags /Fp"${pchpath}" /Yc"${header}" /Tp"${CMAKE_CURRENT_SOURCE_DIR}/${headerpath}"
    COMMENT "Precompiling header ${headerpath} ..."
    SOURCES "${headerpath}"
)

References

Florian
  • 39,996
  • 9
  • 133
  • 149
  • Another very relevant cmake issue is https://gitlab.kitware.com/cmake/cmake/issues/9974 – Niall Douglas Jul 08 '16 at 06:12
  • I'm hesitant to use separate files because the Windows filesystem is particularly slow at creating files when compared to Linux, FreeBSD or OS X. Applied at scale, this technique would make cmake noticeably slower on Windows. Still, thanks for the idea, it could be useful for others. – Niall Douglas Jul 08 '16 at 06:15
  • @NiallDouglas You're welcome. To optimize the turn-around times of CMake enabled projects is btw. an on-going project of mine (see [here](http://stackoverflow.com/questions/37327526/how-to-speed-up-compile-time-of-my-cmake-enabled-c-project)). – Florian Jul 08 '16 at 07:13
  • I'm slowly building out cmake tooling for next gen standards aspiring C++ libraries, so a sort of "Boost 2.0". This cmake tooling is quite a bit different to the usual stuff, it's auto-adapting to any C++ library with a Boost directory layout, and will auto-use C++ Modules etc in the future. It's also very fast as I've only used the fastest cmake programming techniques to traverse deep hierarchies of nested header only libraries. I won't consider it useful to anybody until next year, but you may wish to star https://github.com/ned14/boost-lite and check back from time to time. – Niall Douglas Jul 08 '16 at 07:32