target_link_libraries doesn't add dependency between INTERFACE libraries WITH source generated by custom command
https://cmake.org/cmake/help/latest/command/add_library.html#interface-libraries
If an interface library has source files (i.e. the
SOURCES
target property is set), or header sets (i.e. theHEADER_SETS
target property is set), it will appear in the generated buildsystem as a build target much like a target defined by theadd_custom_target()
command. It does not compile any sources, but does contain build rules for custom commands created by theadd_custom_command()
command.
If a usual target, e.g., a usual library or executable target, is linked by target_link_libraries()
to an interface library with source generated by add_custom_command()
, it implicitly(?) depends on the interface library target and only starts compilation after custom command.
However, if it's also an interface library target WITH source, then there's no such dependency.
Though it can be "fixed" by an additional add_dependencies()
, it seems inconsistent and inconvenient.
Reproducer:
cmake_minimum_required(VERSION 3.28)
project(demo CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_COLOR_DIAGNOSTICS ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_custom_command(
OUTPUT a/a.h
COMMAND mkdir -p a
COMMAND sleep 5
COMMAND echo "struct A{};" > a/a.h
VERBATIM
)
add_library(a INTERFACE EXCLUDE_FROM_ALL "${CMAKE_CURRENT_BINARY_DIR}/a/a.h")
target_include_directories(a INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/a")
add_executable(exe1 EXCLUDE_FROM_ALL main.cpp)
# This is OK, exe1 is depending on `a` and compiled after generating a.h
target_link_libraries(exe1 a)
add_custom_command(
OUTPUT b/b.h
COMMAND mkdir -p b
COMMAND cp a/a.h b/b.h # require a/a.h existing
VERBATIM
)
add_library(b INTERFACE EXCLUDE_FROM_ALL "${CMAKE_CURRENT_BINARY_DIR}/b/b.h")
target_include_directories(b INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/b")
target_compile_definitions(b INTERFACE USE_B)
# This fails, `b` is not depending on `a`
target_link_libraries(b INTERFACE a)
add_executable(exe2 EXCLUDE_FROM_ALL main.cpp)
target_link_libraries(exe2 b)
// main.cpp
#ifdef USE_B
#include <b.h>
using T = B;
#else
#include <a.h>
using T = A;
#endif
int main() {
return sizeof(T);
}
$ rm -rf build
$ CXX=`which clang++` cmake -G"Ninja" -S . -B build
-- The CXX compiler identification is Clang 17.0.4
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /opt/homebrew/opt/llvm/bin/clang++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.4s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/demo/build
$ cmake --build build --target exe1
[5/5] Linking CXX executable exe1
$ cmake --build build --target clean
[1/1] Cleaning all built files...
Cleaning... 8 files.
$ cmake --build build --target exe2
[1/6] Generating b/b.h
FAILED: b/b.h /tmp/demo/build/b/b.h
cd /tmp/demo/build && mkdir -p b && cp a/a.h b/b.h
cp: a/a.h: No such file or directory
[2/6] Generating a/a.h
ninja: build stopped: subcommand failed.
EDIT: To elaborate the actual use case from the reproducer
The custom command and its attached target are provided by our library/package. They're used to preprocess header files, including headers of user's own libraries and third-party libraries. These header files can also depend on other preprocessed third-party libraries.
Previously, when interface library couldn't have source and become a "real" target with attached custom command, it is achieved by three targets, one custom target for attaching custom command, and one interface target for linking with dependencies and another interface target for being linked with dependents. The custom command needs to read compiler options, e.g., INTERFACE_INCLUDE_DIRECTORIES
, INTERFACE_COMPILER_DEFINITIONS
, etc, from the former interface target and read headers of dependencies.
other -> interface_target --> custom_target --> interface_target_dep --> other
\----------------------^
Our provided CMake function, say add_preprocessed_library(tgt headers)
, does something like:
add_custom_command(
OUTPUT ${tgt}.h ...
COMMAND tool ${headers} $<TARGET_PROPERTY:${tgt}_dep,INTERFACE_INCLUDE_DIRECTRIES> ...
DEPENDS ${headers} ...
)
add_custom_target(${tgt}_cmd DEPENDS ${tgt}.h)
add_library(${tgt}_dep INTERFACE)
add_dependencies(${tgt}_cmd ${tgt}_dep)
add_library(${tgt} INTERFACE)
add_dependencies(${tgt} ${tgt}_cmd)
target_link_libraries(${tgt} ${tgt}_dep) # for transitive options
When users want to link preprocessed libraries to other libraries, they have to remember to use ${tgt}_dep
. When they want to link other libraries to preprocessed libraries, they have to remember to target_link_libraries(other [INTERFACE] ${tgt})
AND add_dependencies(other ${tgt})
. Or they have to remember to use our provided processed_target_link_libraries(a b)
. Otherwise, the dependencies are broken.
Then now interface libraries can have source files and become a "real" target, only ONE target would be sufficient:
other --> interface_target(with source ${tgt}.h) --> other
add_custom_command(
OUTPUT ${tgt}.h ...
COMMAND tool ${headers} $<TARGET_PROPERTY:${tgt},INTERFACE_INCLUDE_DIRECTRIES> ...
DEPENDS ${headers} ...
)
add_library(${tgt} INTERFACE ${tgt}.h ...)
That's it, simple and convenient. Users can just use target_link_libraries()
for both cases, except when both targets are preprocessed libraries, as showed by reproducer.