Like many projects we many different libraries, executables, and tests that depend on different combinations of dependencies.
project/src/
- libA/*.{h,c}
- libB/*.{h,c}
- exeA depends_on libA
- testA depends_on lib{A,B}
At the top-level of our project source directory we use:
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
So that targets in the tree can include headers easily like this
#include "libA/interface.h"
This has generally worked very well. But our project has grown now to the point where it is becoming too easy for unintentional header-only dependencies to be created. For example libA
may depend on libB
, but each library may include headers from the other–nothing prevents this mistake and now an unintentional circular dependency exists.
target_link_libraries(libA PUBLIC libB)
src/libA/header.h
>> #include "libB/header.h"
src/libB/header.h
>> #include "libA/header.h"
We could of course remove include_directories(${CMAKE_CURRENT_SOURCE_DIR})
and then re-organize the headers so that we have something like:
src/libA/include/libA/header.h
target_include_directories(libA libA/include)
So that the dependency has to be expressed explicitly in order to expose the headers from the dependent library.
However, we very much prefer to keep all of our source and headers in the same directory.
So I’ve been trying to come up with a way that we can do this dynamically by arrange for these headers to be staged/symlinked at configure-time into the binary directory and setup the target’s include directory in the same way.
I came up with this helper, which for the most part seems to work just fine. Here we do something like
add_cc_library(libA HDRS header.h SRCS src.cc)
And header.h will be includable as libA/header.h
but only when the target is linked explicitly as a dependency.
My question is: Is there a better way to accomplish this? Are there big foot guns I’m not thinking about when considering this approach?
function(add_cc_library NAME)
cmake_parse_arguments(add_cc_library "" "" "HDRS;SRCS;DEPS" ${ARGN})
set(_NAME "add_${NAME}")
cmake_path(GET CMAKE_CURRENT_LIST_DIR STEM include_prefix)
cmake_path(APPEND include_path ${CMAKE_CURRENT_BINARY_DIR} ${NAME} ${include_prefix})
set(manifest "${CMAKE_CURRENT_BINARY_DIR}/${NAME}.manifest")
configure_file(${CMAKE_SOURCE_DIR}/cmake/manifest.in ${manifest} @ONLY)
set(stamp "${manifest}.stamp")
add_custom_command(
OUTPUT ${stamp}
COMMAND ${CMAKE_COMMAND} -E remove_directory ${include_path}
COMMAND ${CMAKE_COMMAND} -E make_directory ${include_path}
COMMAND ${CMAKE_COMMAND} -E touch ${stamp}
DEPENDS ${manifest})
set(staged_headers)
foreach(header ${add_cc_library_HDRS})
set(source_header ${CMAKE_CURRENT_LIST_DIR}/${header})
set(staged_header ${include_path}/${header})
list(APPEND staged_headers ${staged_header})
add_custom_command(
OUTPUT ${staged_header}
COMMAND ${CMAKE_COMMAND} -E create_symlink ${source_header} ${staged_header}
DEPENDS_EXPLICIT_ONLY
DEPENDS ${stamp})
endforeach()
add_library(${_NAME} ${add_cc_library_SRCS} ${staged_headers})
target_link_libraries(${_NAME} PUBLIC ${add_cc_library_DEPS})
target_include_directories(${_NAME} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/${NAME})
add_library("v::${NAME}" ALIAS ${_NAME})
endfunction()