CMake run custom command before build?
Asked Answered
W

3

8

Example source of a binary I want to run before each build, once per add_executable:

#include <stdio.h>

int main(int argc, char *argv[]) {
    for(int i=0; i<argc; ++i)
        printf("argv[%d] = %s\n", i, argv[i]);
    fclose(fopen("foo.hh", "a"));
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(foo_proj)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14")
set(SOURCE_FILES main.cpp)
# ---- this line changes ----
add_executable(foo_proj ${SOURCE_FILES})

Attempts:

add_custom_target(create_foo_hh COMMAND /tmp/bin/create_foo_hh)
add_dependencies(${SOURCE_FILES} create_foo_hh)

Error:Cannot add target-level dependencies to non-existent target "main.cpp". The add_dependencies works for top-level logical targets created by the add_executable, add_library, or add_custom_target commands. If you want to add file-level dependencies see the DEPENDS option of the add_custom_target and add_custom_command commands.

execute_process(COMMAND /tmp/bin/create_foo_hh main.cpp)

No error, but foo.hh isn't created.

How do I automate the running of this command?

Waddell answered 16/6, 2016 at 14:27 Comment(1)
Related: #25688390Waddell
B
10

execute_process() is invoked at configuration time.

You can use add_custom_command():

add_custom_command(
  OUTPUT foo.hh
  COMMAND /tmp/bin/create_foo_h main.cpp
  DEPENDS ${SOURCE_FILES} /tmp/bin/create_foo_hh main.cpp
  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
add_executable(foo_proj ${SOURCE_FILES} foo.hh)

That way, foo.hh is a dependency of foo_proj: and your command will be invoked when building foo_proj. It depends on ${SOURCE_FILES} and /tmp/bin/create_foo_hh main.cpp so that it is generated again if one of those files changes.

Regarding paths, add_custom_command() is configured to run in the current build directory to generate the file there, and include_directories() is used to add the build directory to the include dirs.

Blameful answered 16/6, 2016 at 14:31 Comment(5)
Error:Cannot add target-level dependencies to non-existent target "foo_proj". The add_dependencies works for top-level logical targets created by the add_executable, add_library, or add_custom_target commands. If you want to add file-level dependencies see the DEPENDS option of the add_custom_target and add_custom_command commands.Waddell
@AT My bad, edited my answer; foo.hh has to be specified as a source file when declaring the executable.Blameful
@AT Could you elaborate?Blameful
Sorry, that wasn't meant to post, was getting this ready: gist.github.com/AlecTaylor/e416efbea8793da599513bf28e7d92d7 - 13 line CMakeLists.txt and 7 line main.cpp. It can't find the symbol bar.Waddell
@AT I cannot try to compile your sample, but I guess it is that you have to setup your project to include the generated file; I edited my answer regarding that; also, I'm not sure your echo command properly runs as-is.Blameful
P
1

You probably don't want the custom target to depend on your source files (because they aren't targets themselves and are therefore never "run"), but on the target you create with them:

target_add_dependencies(foo_proj create_foo_hh)
Phyllome answered 16/6, 2016 at 14:30 Comment(0)
P
1

I think that the cleanest is to add two new project() (targets) and then add the resulting file to your final executable. This is how cmake can build a valid dependency tree so when your source files change they get recompiled, the command run, as necessary to get everything up to date.

Build Executable

First, as you do in your example, I create an executable from some .cpp file:

(example extracted from the as2js project)

project(unicode-characters)

add_executable(${PROJECT_NAME}
    unicode_characters.cpp
)

target_include_directories(${PROJECT_NAME}
    PUBLIC
        ${ICU_INCLUDE_DIRS}
        ${SNAPDEV_INCLUDE_DIRS}
)

target_link_libraries(${PROJECT_NAME}
    ${ICU_LIBRARIES}
    ${ICU_I18N_LIBRARIES}
)

As we can see, we can add specific include paths (-I) and library paths (-L). It is specific to that one target so you can have a set of paths that is different from the one used with your other executables.

Generate Additional File

Next, you create a custom command to run your executable like so:

project(unicode-character-types)

set(UNICODE_CHARACTER_TYPES_CI ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.ci)

add_custom_command(
    OUTPUT
        ${UNICODE_CHARACTER_TYPES_CI}

    COMMAND
        unicode-characters >${UNICODE_CHARACTER_TYPES_CI}

    WORKING_DIRECTORY
        ${PROJECT_BINARY_DIR}

    DEPENDS
        unicode-characters
)

add_custom_target(${PROJECT_NAME}
    DEPENDS
        ${UNICODE_CHARACTER_TYPES_CI}
)

Notice a couple of things:

  1. I set a variable (UNICODE_CHARACTER_TYPES_CI) because I am going to reference that one file multiple times

    a. Notice how I put the destination in the binary (cmake output folder) using the ${PROJECT_BINARY_DIR}/... prefix. This is best to avoid generating those files in your source tree (and possibly ending up adding that file to your source tracking system like svn or git). b. An important aspect of the add_custom_command() is the DEPENDS section which includes the name of your special command, the one we defined in the previous step.

  2. The add_custom_target() is what allows cmake to find your target and execute the corresponding command whenever one of the source files (a.k.a. dependency) changes; notice the DEPENDS definition.

Use the Output

Finally, here is the main project (a library in my case) that makes use of the file we generated in the step above.

Notice that I reference that file using the variable I defined in the previous step. That way, when I feel like changing that name, I can do it by simply editing that one variable.

project(as2js)

configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/version.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/version.h
)

add_library(${PROJECT_NAME} SHARED
    compiler/compiler.cpp
    ...
    parser/parser_variable.cpp
    ${UNICODE_CHARACTER_TYPES_CI}

    file/database.cpp
    ...
)

(Note: the ... represent a list of files, shorten for display here as these are not important, the link above will take you to the file with the complete list.)

By having the filename inside the list of files defined in the add_library() (or the add_executable() in your case), you create a dependency which will find your custom_target(), because of the filename defined in the OUTPUT section of the add_custom_command()¹.


¹ It is possible to defined multiple outputs for an add_custom_command(). For example, some of my generators output a .cpp and a .h. In that case, I simply define both files in the OUTPUT section.

Results

Important points about the final results with this solution:

  1. the output files of your generator are saved in the binary output path instead of your current working directory
  2. the Makefile generated by cmake includes all the necessary targets/dependencies which means changing any of the input files regenerate everything as expected (even if you just update a comment)
  3. if the generator fails, the build fails as expected
  4. the files are generated by the build step (make time) instead of the generation step (cmake time, like the execute_process() would do)
Poirier answered 18/2, 2023 at 15:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.