Best practice to run a prebuild step with CMake

3.3k Views Asked by At

Suppose you have a repository with a folder (named dataset) with several .csv files and a python script (named csvcut.py) that takes all the .csv in dataset and generates corresponding .h files.

Those .h files are included in some .cpp files to build an executable (add_executable(testlib...) used for testing.

Suppose you use add_custom_target(test_pattern... to make a target (named test_pattern) that runs csvcut.py, and add_dependencies(testlib test_pattern) to run the script before building testlib.

This works, but it would be better if:

  • the script was run only when the files in dataset folder or the script itself changes (not when .cpp changes);
  • the .h files was generated in a subfolder of the build folder (i.e. build/tests/dataset/), and included in the .cpp files like so #include <tests/dataset/generated.h>.

Do you have any suggestions for making these improvements / optimizations?

Thanks, Alberto

1

There are 1 best solutions below

1
Stephen Newell On BEST ANSWER

This requires multiple steps, but can all be handled with standard CMake. First, we'll use add_custom_command to actually generate the files. I'm also adding a custom target, but only since I couldn't figure out how to make an INTERFACE library work without it.

add_custom_command(
    OUTPUT
        "${CMAKE_CURRENT_BINARY_DIR}/include/foo.h"
    COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/gen.py"
    DEPENDS
        gen.py
        foo.h.in
    WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include"
)
add_custom_target(gen_files
    DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/foo.h"
)

For my case, gen.py just spits out a basic header file, but it shouldn't matter. List whatever files you need as output, and your csv file should be under DEPENDS (for me, foo.h.in tries to simulate this).

Since you only mentioned generating header files, I created an INTERFACE library that depends on the gen_files target. I also added the appropriate include directory.

add_library(foo INTERFACE)
target_include_directories(foo
    INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/include"
)
add_dependencies(foo
    gen_files
)

If building a STATIC/SHARED library, I was able to add the generated files directly as sources and dependencies worked, but the INTERFACE library required the extra target (even when I tried listing the files under add_dependencies). Since you already have a custom target, I assume this won't be a huge issue.

Lastly, I have an executable that links against foo.

add_executable(main
    main.c
)
target_link_libraries(main
    PRIVATE
        foo
)

Demo:

$ make clean 
$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  include  Makefile
$ make
[ 33%] Generating include/foo.h
[ 33%] Built target gen_files
[ 66%] Building C object CMakeFiles/main.dir/main.c.o
[100%] Linking C executable main
[100%] Built target main
$ make clean
$ make main
[ 33%] Generating include/foo.h
[ 33%] Built target gen_files
[ 66%] Building C object CMakeFiles/main.dir/main.c.o
[100%] Linking C executable main
[100%] Built target main
$ make
[ 33%] Built target gen_files
[100%] Built target main
$ touch ../foo.h.in 
$ make
[ 33%] Generating include/foo.h
[ 33%] Built target gen_files
[100%] Built target main
$ touch ../gen.py 
$ make
[ 33%] Generating include/foo.h
[ 33%] Built target gen_files
[100%] Built target main
$ ls include/
foo.h

If either the input (foo.h.in) or generation script (gen.py) change, the targets are rebuilt.