JPNotADragon
JPNotADragon

Reputation: 2210

CMake: header-only library with generated files

I have a library that needs to carry some constant data injected from the content of non-source files (in this case, OpenGL shader code). To achieve this, I'm using add_custom_command() to generate include files that I can then #include into my code to initialize const static variables.

This works perfectly with regular libraries (static or shared), but now I'd like to make my library header-only. The ability of C++ to let static methods return static data without running the risk of having that data duplicated in each translation unit ("magic statics") makes this possible.

The problem however is that CMake seems to assume that an INTERFACE library (which is the CMake feature that I'm using to create header-only libraries) does not need building - which, in this case, is wrong.

(I realize that there is no actual obligation for my library to be header-only. In this particular case, the reason I want this is that I would like the library, which is doing OpenGL, to remain independent of any specific binding library [such as GLEW or GLee or the newcomer glbinding]. By keeping my library header-only, I can leave that choice to the user - all he needs to do is #include the header of the binding library before mine.)

Does anyone see a way to have CMake trigger the header-generating custom commands, at the latest when the consumer project is being built?

EDIT: I just realized that I could have the "best of both worlds" as it were by keeping my library static but still keeping all my code except for the constant data in the header files. That way, there would still be no need to choose a specific OpenGL binding library. However, there are still advantages to having a library be header-only - simplicity of use for one - so I'm leaving my question open.

EDIT #2: Here is the relevant part of my CMakeLists.txt file (I only stripped the library dependencies - all header-only - from the end):

set(SHADER_FILES "src/vertex.glsl" "src/fragment.glsl")

add_library(libGPCGUIGLRenderer INTERFACE)
target_sources(libGPCGUIGLRenderer INTERFACE ${SHADER_FILES})

target_include_directories(libGPCGUIGLRenderer BEFORE
  INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# Embed shader files

source_group("Shader files" FILES ${SHADER_FILES})

set(GENERATED "${CMAKE_CURRENT_BINARY_DIR}/generated")
target_include_directories(libGPCGUIGLRenderer INTERFACE ${GENERATED})

# Find the GPC Bin2C utility
find_package(GPCBin2C REQUIRED)

# Add a custom target and a dependency for each shader file    
foreach(shader ${SHADER_FILES})
  get_filename_component(name "${shader}" NAME)
  set(shader_header "${GENERATED}/${name}.h")
  add_custom_command(
    OUTPUT ${shader_header}
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${shader}
    COMMAND GPCBin2C --input=${CMAKE_CURRENT_SOURCE_DIR}/${shader} --output=${shader_header}
  )
  target_sources(libGPCGUIGLRenderer INTERFACE ${shader_header})
endforeach()

Upvotes: 14

Views: 8174

Answers (4)

Martin Gerhardy
Martin Gerhardy

Reputation: 1990

You should use this:

set_source_files_properties(your_generated.h PROPERTIES LANGUAGE CXX)

But beware that this might still fail on different generators - Xcode doesn't like it that way. But all other generators I've tried were working properly.

https://cmake.org/cmake/help/latest/prop_sf/LANGUAGE.html

The only thing that really helps for all generators is to make them cpp files - and include those instead of the headers.

EDIT

I just found out that it is enough to just add a single cpp file to my object target to make the headers begin generated - for all generators.

function(engine_generated_library TARGET)
    add_library(${TARGET} OBJECT)
    set_target_properties(${TARGET} PROPERTIES LINKER_LANGUAGE CXX)
    write_file(${GENERATE_DIR}/${TARGET}.cpp "#include <stdio.h>")
    set_source_files_properties(${GENERATE_DIR}/${TARGET}.cpp PROPERTIES GENERATED TRUE)
    target_sources(${TARGET} PRIVATE ${GENERATE_DIR}/${TARGET}.cpp)
    add_dependencies(codegen ${TARGET})
endfunction()

now you can add source files to your target...

Upvotes: 0

Unapiedra
Unapiedra

Reputation: 16197

Creating a static library with headers as the only sources worked for me. It is, of course, only a work-around.

  • Creating a static library with only header files results in an empty library. Mine says !<arch> as the only content.
  • CMake will automatically get the dependencies correct across sub-directories.
  • Since all sources are headers, you need to tell CMake which linker language should be used.

Code:

set(OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/generated_include")
add_custom_command(
    OUTPUT "${OUTDIR}/outfile.h"
    # Replace the next two lines with a proper generating script.
    COMMAND mkdir -p ${OUTDIR}
    COMMAND touch ${OUTDIR}/outfile.h
)

# Note, I am only adding header files to the library.
add_library(generated-headers STATIC 
    "${OUTDIR}/outfile.h"
)
set_target_properties(generated-headers
    PROPERTIES LINKER_LANGUAGE CXX)
target_include_directories(generated-headers PUBLIC ${OUTDIR})

Use in other directories like this:

# In any other directory of the same CMake project:
add_executable(main main.cpp)
target_link_libraries(main generated-headers)

Tested on CMake 3.2, 3.8 and 3.9. Using Ninja and Make generators.

Upvotes: 8

GPMueller
GPMueller

Reputation: 3149

I ran into comparable problems when trying to use glad: https://github.com/Dav1dde/glad

It uses a custom CMake command to build a binding, which means the files you need to include in the project which uses glad do not exist, so that CMake does not build glad (which would create those files)...

I did not get to try it yet, but example 3 of the following link seems to be a good solution and I believe it may work in your case: https://samthursfield.wordpress.com/2015/11/21/cmake-dependencies-between-targets-and-files-and-custom-commands/

Upvotes: 1

steveire
steveire

Reputation: 11074

You can use target_sources in CMake 3.1 to tell consumers to compile interface files:

add_library(source_only INTERFACE)
target_sources(source_only INTERFACE foo.cpp) 

http://www.cmake.org/cmake/help/v3.1/command/target_sources.html

Upvotes: 3

Related Questions