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
)
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
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
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
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?
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
)
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
)
So, learned lesson: to run a custom command, a custom target must depend on it.
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
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
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
)
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)
Nice to see that I am not the only one writing about CMake here :)
Nice series, good job!
Thank you very much, you saved my day