Introduction
In this post, we explore how CMake manages dependencies and automates custom build tasks using add_custom_command()
and add_custom_target(). We
learn how to streamline your build process with examples of file generation, external tool integration, and dependency handling.
Points on dependency
An entity (target or command) is out of date, if any of its dependencies has changed since the last build. CMake always builds/runs an out of date entity.
If CMake flow reaches an entity that needs to be built or run, the dependencies of the entity are checked. There are target-level dependency and file-level dependency:
- If the entity is dependent on a target, and the target is not built yet, the target build is triggered. If the target is already built, cmake checks if the target is out of date.
- If an entity is dependent on a file, it means that the entity is built or run, after the file is created. If the file is already there, CMake checks if the file is out of date.
add_custom_command
I break it into two sections as It can be used in two ways: attached to a target and standalone to output a file. They are explained in the following sections.
Attached to a target
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
add_executable(circle main.cpp)
add_custom_command(
TARGET circle
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:circle> /destination/path
)
Let’s explain each line:
Target circle
: means this command is run around the time circle is built.POST_BUILD
: means run this command after building circle. There are other options, butPOST_BUILD
is most likely what you need.COMMAND ...
: the actual command to be run${CMAKE_COMMAND}
: is cmake executable in the unix/Windows/… terminal in a cross platform way. Instead of CMake executable, you can write any other terminal command or name of any executable target.-E
: to Run a cross-platform command likecopy
,echo
,make_directory
, see more in CMake guide.$<TARGET_FILE:circle>
: a generator expression which gives the path of cross-platform executable file of circle target./destination/path
: replace this path with the copy destination path. Paths in a command are full.
To create a rule to produce a file
The command is only triggered if a target depends on that output file. The target, before being built, runs the command to produce the needed file.
In the example below, we have a chain of actions: target writer is created, a command runs the writer executable, which creates a cpp file. That cpp file is then used to build another target.
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
add_executable(writer main.cpp)
add_custom_command(
OUTPUT ${PROJECT_BINARY_DIR}/square.cpp
DEPENDS writer
COMMAND writer ${PROJECT_BINARY_DIR}/square.cpp
)
add_executable(square ${PROJECT_BINARY_DIR}/square.cpp)
Let’s explain the lines:
OUPUT square.cpp
: specifiying the path of output file.DEPENDS writer
: This commands can trigger targetwriter
to be built if it is not built or out of date. This also means, there is a file-level dependency to the executable of writer, i.e. the command runs only if the executable has changed during current build.COMMAND ...
: the command that creates square.cpp file.
add_custom_target
I just want to reminde you that add_executable()
and add_library()
are used to
compile source codes to create executables and libraries. However, add_custom_target()
is for creating or running
anything else, for example, creating custom clean, integrated external tools, creating documentation, grouping dependencies and running tests.
add_custom_target()
is usually used with add_custom_command()
. The custom target is made dependent on some custom commands
which are run for building the custom target:
add_custom_command(
OUTPUT ${PROJECT_BINARY_DIR}/page.html
DEPENDS ${PROJECT_SOURCE_DIR}/page.txt
COMMAND ${text2html} ${PROJECT_SOURCE_DIR}/page.txt
)
add_custom_command(
OUTPUT ${PROJECT_BINARY_DIR}/page.pdf
DEPENDS ${PROJECT_BINARY_DIR}/page.html
COMMAND ${html2pdf} ${PROJECT_BINARY_DIR}/page.html
)
add_custom_target(PdfMaker ALL
DEPENDS ${PROJECT_BINARY_DIR}/page.pdf
)
For a custom target Keyword ALL
is necessary to add it to default build target. Otherwise, you have to specifically ask cmake in terminal to build it.
The PdfMaker is a target and must be built. It is dependent on the second command, the second command is
dependent on the first command. Therefore, this chain of actions is triggered:
first command -> second command -> target PdfMaker
A custom target can also have commands in its definition with COMMAND
keyword, however, the key difference between
add_custom_target()
and a detached add_custom_command()
is that the latter has an output file which can be used as
a dependency. However, for add_custom_target()
there is no OUTPUT
keyword to create a file dependency.
Another related conequence is that as add_custom_target()
is not expected to build an actual file, there is nothing to compare
if the output of the target is out of date. Therefore, add_custom_target()
is always out of date, which means, its its out of date commands are
executed everytime a project is built.
Note that add_executable()
and add_library()
cannot define dependencies directly similar to a custom target.
To create a build order where we have libraries, executables and custom targets,
we can introduce dependencies using add_dependencies()
.
Example for re-run of custom command
In the example below, the custom target, write
, is dependent on the custom command.
By building the project, the command is only run
if its dependency, input.txt, is present and is not changed since that last build. To run this example, create input.txt
file in the build
directory. To make the command re-run everytime you build the project, you just need to edit the content of input.txt
before each build.
Each time the command executed, a Hello, world!
is appended to generated_file.txt
.
cmake_minimum_required(VERSION 3.23)
project(example LANGUAGES CXX)
add_custom_command(
OUTPUT ${PROJECT_BINARY_DIR}/generated_file.txt
COMMAND echo "Hello, World!" >> ${PROJECT_BINARY_DIR}/generated_file.txt
DEPENDS ${PROJECT_BINARY_DIR}/input.txt
COMMENT "Generating file"
)
add_custom_target(write ALL
DEPENDS ${PROJECT_BINARY_DIR}/generated_file.txt)