1

I have a situation where I need to run a script when a CMake target is linked-to so that it can automatically generate files in the current project directory that are used to interface with the library.

I know when you link to a CMake target it automatically pulls in the headers for the library so they become visible to the compiler, but I need it to also generate some files within the directory of the linkee that will also be visible to the compiler upon building.

How can I tell CMake that I want to run a script to generate the files every time my_cmake_target is linked to?

Example of linking in CMakeLists.txt:

target_link_libraries(my_executable PRIVATE my_cmake_target)

I want the command to run at the same time that CMake transitively updates the include directories based on the target passed to "target_link_libraries". (Before any building/linking actually takes place)

See here for more info on how that works:

https://schneide.blog/2016/04/08/modern-cmake-with-target_link_libraries/

Using target_link_libraries to link A to an internal target B will not only add the linker flags required to link to B, but also the definitions, include paths and other settings – even transitively – if they are configured that way.

tjwrona1992
  • 8,614
  • 8
  • 35
  • 98
  • 1
    Maybe this will help you https://cmake.org/cmake/help/latest/command/add_custom_command.html#build-events – u-235 Jan 24 '22 at 21:10
  • Lol I was literally just reading that page immediately before I got your comment. Sadly that looks like it won't run if the target has already been built. "The command becomes part of the target and will only execute when the target itself is built. If the target is already built, the command will not execute." I need it to run every time I link to it whether it has already been built or not. – tjwrona1992 Jan 24 '22 at 21:15
  • I have edited your answer to clarify your question in light of your comment. If I got anything wrong, please update with more detail. – Alex Reinking Jan 24 '22 at 22:05
  • @AlexReinking That's not quite what I was looking for. I updated it to be more clear though. Basically I want it to generate the files at the same point where CMake transitively pulls in the include paths for the target. (before any building/linking takes place) – tjwrona1992 Jan 24 '22 at 22:47
  • That point is very hard to hook into... you basically only have `file(GENERATE)` as an option. If you gave more detail about how these files are created, where they should be located, and what their contents are, we might be better able to help you. – Alex Reinking Jan 24 '22 at 22:54
  • @AlexReinking The script would read a file in the current directory and generate two more files in that same directory based on the contents of the first file. The only reason linking to that target even matters is that you don't need the autogenerated files unless you're already planning on linking to the target. So I don't want to generate them when someone isn't using that target. – tjwrona1992 Jan 24 '22 at 22:56
  • The script is something imported, then? Either an `IMPORTED` target or a shell script, Python script, etc. in the source tree? Also, which file? Is it something that can be determined by the target's name or one of its properties? Finally, why _can't_ it happen during the build? Do they all need to be generated up-front? – Alex Reinking Jan 24 '22 at 22:57
  • @AlexReinking The script is located in the folder of the target I am linking to. It is a Perl script. The file that the script reads is in the current directory and is something that follows a standard naming convention so the script knows which file to read in based on its name. It can't happen during the build because the script generates .cpp/.h files used by the build. – tjwrona1992 Jan 24 '22 at 23:43

1 Answers1

1

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:

  1. 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
Alex Reinking
  • 16,724
  • 5
  • 52
  • 86
  • Unfortunately the target will be outside of the current directory (It will be part of the same CMake parent project though so I'm not sure if that helps at all). This still gives me a lot of useful information to think about though so +1 for the very well thought out response. – tjwrona1992 Jan 24 '22 at 22:46
  • You might be able to fix the directory-local thing using `cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL MyProj_post_build)` and then recursively walking the `SUBDIRECTORIES` directory property to traverse the whole tree. That was a bit too much complexity to code and test, though. – Alex Reinking Jan 24 '22 at 22:52
  • Yeahh... I'm leaning towards just making a function in a .cmake file that you have to include and call manually at this point. But I was hoping to avoid that. – tjwrona1992 Jan 24 '22 at 22:54
  • The lack of magic in that more conventional approach is a pro. – Alex Reinking Jan 24 '22 at 22:55
  • @tjwrona1992 - I've updated my answer to be less limited by abusing the compatible interface properties feature. It is now able to determine whether to run the command accurately on any type of target, including libraries, and including transitively. – Alex Reinking Jan 24 '22 at 23:11
  • I have again updated it to work from any directory (but still only once). – Alex Reinking Jan 24 '22 at 23:26
  • Thank you for all of the hard work. I will take some time reading through the CMake documentation trying to fully understand what this does haha – tjwrona1992 Jan 25 '22 at 00:07