DEV Community

Cover image for Learning CMake 3: creating custom targets
Israel Blancas
Israel Blancas

Posted on • Updated on

Learning CMake 3: creating custom targets

Learning CMake 3: understanding add_custom_command and add_custom_target

During the previous chapters we learn how to create targets. These targets will build one library or one application. Sometimes it is needed to generate code, copy files or run some commands that are not just compile or link one binary. How can we do that?

The add_custom_target CMake macro

As the documentation says, this macro "Adds a target with the given name that executes the given commands". So, you can create a CMake target that will execute a command. Imagine we have the following CMakeLists.txt file:

cmake_minimum_required(VERSION 3.12)
add_custom_target(my_custom_target
    COMMAND
        ${CMAKE_COMMAND} -E echo Hi this is a custom target
    VERBATIM
)
Enter fullscreen mode Exit fullscreen mode

There is something really interesting here. The CMake variable CMAKE_COMMAND contains the path to the CMake executable we are using right now. That allow us to use all the commands provided by CMake when the argument -E is provided. In this case, we will print "Hi this is a custom target". But you can use this mechanism to do different things like copying a file, creating a folder or calculate the SHA256 checksum of a file.

Also, we provided the VERBATIM argument to the CMake macro. Why? Because this command can be interpret diferently depending on the OS where it is executed. The VERBATIM will make the command to be the same in all the platforms. You can find the full explanation in the official CMake documentation.

If we run cmake and we build the target my_custom_target, the output will be:

➜  ~/  mkdir build
➜  ~/  cd build
➜  ~/build  cmake ..
-- The C compiler identification is GNU 8.3.1
-- The CXX compiler identification is GNU 8.3.1
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/israel/build
➜  ~/build  cmake --build . --target my_custom_target
Scanning dependencies of target my_custom_target
Hi this is a custom target
Built target my_custom_target
Enter fullscreen mode Exit fullscreen mode

Important 1: take into account that this custom target will not be part of the target all. So, if you try to build the target all, nothing will happen.

➜  ~/build  cmake --build .
➜  ~/build
Enter fullscreen mode Exit fullscreen mode

You need to build explicitly your target. Why? You need to make your custom target depend on the target all or in another target depending on the target all. You can do this with the add_dependencies CMake macro.

Important 2: what will happen if we try to build the my_custom_target target again?

➜  ~/build  cmake --build . --target my_custom_target
Hi this is a custom target
Built target my_custom_target
Enter fullscreen mode Exit fullscreen mode

As you can see, the command was executed again. Let's imagine we want to run the my_custom_target target only if is needed. How can we do this?
Efficiency

The add_custom_command CMake macro

The documentation of the add_custom_command CMake macro says: "This defines a command to generate specified OUTPUT file(s). What does it mean? The command will generate some output files only if needed.

Ok, let's create a custom command to generate one file:

cmake_minimum_required(VERSION 3.12)
add_custom_command(
    OUTPUT
        "${CMAKE_CURRENT_BINARY_DIR}/generated_file"
    COMMAND
        ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/generated_file
)
Enter fullscreen mode Exit fullscreen mode

Take into account that we are generating the new file in the binary directory (the path is relative to the CMAKE_CURRENT_BINARY_DIR CMake variable).

If we call CMake, we will not have a way to call this custom command. Why? Because in CMake we build targets. Since there was not a defined target, the custom command will not be executed. So, we have to create a custom target and create a dependency between our custom target and our custom command:

cmake_minimum_required(VERSION 3.12)

add_custom_target(my_custom_target
    DEPENDS
        "${CMAKE_CURRENT_BINARY_DIR}/generated_file"
)

add_custom_command(
    OUTPUT
        "${CMAKE_CURRENT_BINARY_DIR}/generated_file"
    COMMAND
        ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/generated_file
)
Enter fullscreen mode Exit fullscreen mode

So, learned lesson: to run a custom command, a custom target must depend on it.

Dependency

If we are generating more than one file, we have to list all of them in the OUTPUT argument for add_custom_command and in the DEPENDS argument for add_custom_target. Doing this, we will ensure that all the files will be generated. If try to build my_custom_target:

➜  ~/  mkdir build
➜  ~/build  cmake ..
-- The C compiler identification is GNU 8.3.1
-- The CXX compiler identification is GNU 8.3.1
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/israel/build
➜  ~/build  cmake --build . --target my_custom_target
Scanning dependencies of target my_custom_target
[100%] Generating generated_file
[100%] Built target my_custom_target
➜  ~/build
cmake_install.cmake  CMakeFiles      Makefile
CMakeCache.txt       generated_file
Enter fullscreen mode Exit fullscreen mode

We can see that the file generated_file was generated properly. If we run the build again:

cmake --build . --target my_custom_target
[100%] Built target my_custom_target
Enter fullscreen mode Exit fullscreen mode

The target was built but the command was not executed again.

We can improve the process. Imagine that we need to call this command only if something changed in the source code. We need to add this file to the DEPENDS argument of add_custom_command:

cmake_minimum_required(VERSION 3.12)

add_custom_target(my_custom_target
    DEPENDS
        "${CMAKE_CURRENT_BINARY_DIR}/generated_file"
)

add_custom_command(
    OUTPUT
        "${CMAKE_CURRENT_BINARY_DIR}/generated_file"
    COMMENT
        "This is generating my custom command"
    COMMAND
        ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/generated_file
    DEPENDS
        ${CMAKE_CURRENT_SOURCE_DIR}/source_file
)
Enter fullscreen mode Exit fullscreen mode

In this case, the custom command will be run:

  • The first time
  • Each time the source_file file placed in the source directory is modified

Why is this useful? Imagine that you have to call a program to generate code. Hundreds of files. Hundreds of calls to that program. If nothing changes... why do you need to regenerate all that code. You can save a lot of time!

Important: if a custom target depends on a custom command, the custom target will run the custom command. If the custom command contains also a command, it will be executed always and after running the custom command.

Recap

When is needed to use add_custom_target?
Each time we need to run a command to do something in our build system different to build a library or an executable.

When is a good idea to run a command in add_custom_target?
When the command must be executed always the target is built.

When is a good idea to use add_custom_command?
Always we want to run the command when is needed: if we need to generate a file (or more) or regenerate it if something changed in the source folder.

Extra info

CMake has its own template engine that you can use with the configure_file CMake macro. When is a good idea to use it to generate files? And how about using execute_process to run a command in CMake?

It is fine to use these ways to generate files and execute commands but these actions will take place only during the build system generation. So, if you need to run these processes each time you change something in the source directory (because you are developing, for instance), you will need to run CMake and the native tool instead of just calling the native tool and let it do its magic (solving all the dependencies automatically).

Top comments (2)

Collapse
 
pgradot profile image
Pierre Gradot

Nice to see that I am not the only one writing about CMake here :)

Nice series, good job!

Collapse
 
rmalca profile image
Rodrigo Malca

Thank you very much, you saved my day