Unfortunately, there's nothing built-in to help you do this. Propagating custom commands through interface properties is not something CMake has implemented (or has plans to, afaik).
However, and this is kind of cursed, here is a way.
You create a function that scans the directory for targets that link to your special library. For each one of those targets, it attaches a special source file in the binary directory and a command for generating that file. It uses a custom property (here, MAGIC
) for determining whether to actually generate the source file and include it in your target's sources.
Then, use cmake_language(DEFER CALL ...)
to run that function at the end of the current directory's build script. This part ensures the function does not have to be called manually, even in find_package
scenarios.
TODOS:
- Running this code twice will likely cause errors. However, you can avoid problems by marking whether a target has already been processed with another bespoke property.
# ./CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(example LANGUAGES CXX)
add_subdirectory(subdir)
add_executable(my_executable main.cpp)
target_link_libraries(my_executable PRIVATE my_cmake_target)
add_executable(excluded main.cpp default-name.cpp)
# ./subdir/CMakeLists.txt
function (MyProj_post_build)
set(dirs ".")
while (dirs)
list(POP_FRONT dirs dir)
get_property(subdirs DIRECTORY "${dir}" PROPERTY SUBDIRECTORIES)
list(APPEND dirs ${subdirs})
get_property(targets DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS)
foreach (target IN LISTS targets)
# Do whatever you want here, really. The key is checking
# that $<BOOL:$<TARGET_PROPERTY:MAGIC>> is set on the
# target at generation time. I use a custom command here,
# but you could use file(GENERATE).
add_custom_command(
OUTPUT "MyProj_${target}.cpp"
COMMAND "${CMAKE_COMMAND}" -E echo "const char* Name = \"$<TARGET_PROPERTY:${target},NAME>\";" > "MyProj_${target}.cpp"
VERBATIM
)
target_sources(
"${target}"
PRIVATE
"$<$<BOOL:$<TARGET_PROPERTY:MAGIC>>:$<TARGET_OUT/MyProj_${target}.cpp>"
)
endforeach ()
endwhile ()
endfunction ()
cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL MyProj_post_build)
add_library(my_cmake_target INTERFACE)
set_target_properties(my_cmake_target PROPERTIES INTERFACE_MAGIC ON)
set_property(TARGET my_cmake_target APPEND PROPERTY COMPATIBLE_INTERFACE_STRING MAGIC)
// main.cpp
#include <iostream>
extern const char* Name;
int main () { std::cout << Name << "\n"; }
// default-name.cpp
const char* Name = "default";
Here's proof it works...
$ cmake -G Ninja -S . -B build
[1/7] cd /home/alex/test/build && /usr/bin/cmake -E echo "const char* Name = \"my_executable\";" > MyProj_my_executable.cpp
[2/7] /usr/bin/c++ -MD -MT CMakeFiles/excluded.dir/default-name.cpp.o -MF CMakeFiles/excluded.dir/default-name.cpp.o.d -o CMakeFiles/excluded.dir/default-name.cpp.o -c /home/alex/test/default-name.cpp
[3/7] /usr/bin/c++ -MD -MT CMakeFiles/my_executable.dir/MyProj_my_executable.cpp.o -MF CMakeFiles/my_executable.dir/MyProj_my_executable.cpp.o.d -o CMakeFiles/my_executable.dir/MyProj_my_executable.cpp.o -c /home/alex/test/build/MyProj_my_executable.cpp
[4/7] /usr/bin/c++ -MD -MT CMakeFiles/excluded.dir/main.cpp.o -MF CMakeFiles/excluded.dir/main.cpp.o.d -o CMakeFiles/excluded.dir/main.cpp.o -c /home/alex/test/main.cpp
[5/7] /usr/bin/c++ -MD -MT CMakeFiles/my_executable.dir/main.cpp.o -MF CMakeFiles/my_executable.dir/main.cpp.o.d -o CMakeFiles/my_executable.dir/main.cpp.o -c /home/alex/test/main.cpp
[6/7] : && /usr/bin/c++ CMakeFiles/my_executable.dir/main.cpp.o CMakeFiles/my_executable.dir/MyProj_my_executable.cpp.o -o my_executable && :
[7/7] : && /usr/bin/c++ CMakeFiles/excluded.dir/main.cpp.o CMakeFiles/excluded.dir/default-name.cpp.o -o excluded && :
$ ./build/my_executable
my_executable
$ ./build/excluded
default