Custom Targets and Build Utilities
Learn to create standalone, non-code targets using add_custom_target() for utility tasks like code formatting and documentation generation.
In the last lesson, we learned how add_custom_command()
can inject external scripts into our build process. We saw that it's perfect for tasks that are an integral part of building a specific target, like copying a binary after it's built.
However, not every task is directly related to compiling code. Developers often need to perform utility actions that aren't part of the standard build. Common examples include:
- Running a code formatter like
clang-format
across the entire codebase. We'll walk through this as a practical example of these techniques in the next lessons. - Generating documentation with a tool like Doxygen, which we cover later in the chapter.
- Running a static analysis tool to check for code quality issues.
- Any recurring task in our workflow, like copying all the files in one directory to another.
These actions don't produce a library or an executable, but they are still valuable parts of the development workflow. This is where custom targets come in. This lesson will teach you how to use add_custom_target()
to create these standalone, command-driven targets.
Phony Targets
If you've ever used a Makefile
, you're likely familiar with targets like make clean
or make install
. These are called phony targets because they don't correspond to an actual file named clean
or install
. They are simply named shortcuts for a sequence of commands.
The add_custom_target()
command in CMake creates the equivalent of a phony target. It defines a new target that you can "build" by name, but unlike an executable or library, it's not expected to produce an output file. Its only purpose is to execute one or more commands.
The key distinction is:
add_custom_command()
attaches a command to an existing target or file. It's part of the process of building something else.add_custom_target()
creates a new, standalone target.
Creating a Utility with add_custom_target()
The syntax for creating a custom target is straightforward:
- We provide a name for our target -
say_hello
in the following example - An optional
COMMENT
, which can describe what our target is for and may be displayed in the terminal when we "build" our target - We provide one or more commands to run using the
COMMAND
keyword
Here's a basic target that just logs some messages to the terminal:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_subdirectory(app)
add_custom_target(say_hello
COMMENT "Saying hello to the developer..."
COMMAND ${CMAKE_COMMAND} -E echo "Hello there!"
COMMAND ${CMAKE_COMMAND} -E echo "This is a second command"
)
By default, a custom target is not part of the normal build. You have to explicitly ask to build it. This makes it perfect for optional developer utilities.
Building a Custom Target
After configuring our project, we can run our new target using the --target
flag as part of the build step:
cmake --build . --target say_hello
This command tells the build system, "Ignore the default build process and just build the target named say_hello
." In this example, you may see the comment we provided, in addition to the execution of our two echo
commands:
[1/1] Saying hello to the developer...
Hello there!
This is a second command
The idea here is that we provide a simple, memorable command (--target say_hello
) for a common developer task that might otherwise require more effort to complete. We'll show some practical examples of this throughout the rest of the chapter
CMake-Provided Targets
By default, CMake also provides some utility targets for us. For example, the clean
target deletes our compiled outputs, thereby forcing them to be recompiled from scratch on the next build:
cmake --build . --target clean
[1/1] Cleaning all built files...
Cleaning... 4 files.
The help
target lists all of the available targets, both the targets that CMake provides by default, as well as any targets we have added to our CMakeLists.txt
files:
cmake --build . --target help
The ALL
Keyword
What if you want a custom target to run as part of the default build? You can achieve this by adding the ALL
keyword to its definition.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_subdirectory(app)
add_custom_target(print_message ALL
COMMENT "Printing a message..."
COMMAND ${CMAKE_COMMAND} -E echo
"This message also prints on the default build"
)
add_custom_target(say_hello
COMMENT "Saying hello to the developer..."
COMMAND ${CMAKE_COMMAND} -E echo "Hello there!"
COMMAND ${CMAKE_COMMAND} -E echo "This is a second command"
)
We can still run this target by itself in the usual way:
cmake --build . --target print_message
[1/1] Printing a message...
This message also prints on the default build
But it will also run as part of the default build:
cmake --build .
...
[3/5] Printing a message...
This message also prints on default builds
....
In this example, our target was built as step 3 in a 5-part build, but we'll learn how to control this later in the lesson.
Building Custom Targets with a Preset
Within a build preset, we can specify which targets we want to be built using a targets
array. Creating a preset to build both of our new targets would look like this:
CMakePresets.json
{
"version": 3,
"configurePresets": [{
"name": "base",
"binaryDir": "${sourceDir}/build"
}],
"buildPresets": [{
"name": "print_stuff",
"configurePreset": "base",
"targets": ["say_hello", "print_message"]
}]
}
We can use this preset as part of the build step in the usual way. Remember, when using a preset, we should run the command from the root of our project, where our CMakePresets.json
file is.
The configure step looks like this:
cmake --preset base
Followed by the build step:
cmake --build --preset print_stuff
[1/2] Printing a message...
This message also prints on the default build
[2/2] Saying hello to the developer...
Hello there!
This is a second command
Declaring Dependencies with add_dependencies()
A custom target is a node in CMake's dependency graph just like any other target. As we've seen, commands like target_link_libraries()
can automatically create these links as a side effect.
However, for custom targets that don't need to be "linked" in the C++ sense, we can directly set up their dependencies using the add_dependencies()
command.
In the following contrived example, we state that print_message
depends on say_hello
:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_subdirectory(app)
add_custom_target(print_message ALL
COMMENT "Printing a message..."
COMMAND ${CMAKE_COMMAND} -E echo
"This message also prints on the default build"
)
add_custom_target(say_hello
COMMENT "Saying hello to the developer..."
COMMAND ${CMAKE_COMMAND} -E echo "Hello there!"
COMMAND ${CMAKE_COMMAND} -E echo "This is a second command"
)
add_dependencies(print_message say_hello)
Now, when we run our build, CMake will respect this dependency. It ensures say_hello
is built before print_message
:
cmake --build --preset=print_stuff
[1/2] Saying hello to the developer...
Hello there!
This is a second command
[2/2] Printing a message...
This message also prints on the default build
The DEPENDS
Argument
When our target is being created by add_custom_target()
, we don't need to use add_dependencies()
. Instead, we provide the dependencies as arguments to add_custom_target()
if we prefer. We do this using the DEPENDS
keyword, followed by one or more target names:
add_custom_target(print_message ALL
COMMENT "Printing a message..."
DEPENDS say_hello
COMMAND ${CMAKE_COMMAND} -E echo
"This message also prints on the default build"
)
Executing Targets
The tools we use as COMMAND
arguments within add_custom_target()
and add_custom_command()
can be targets within our build.
To set this up, we simply provide the name of the target. In the following example, we add a run
target that executes the GreeterApp
application that CMake is already managing:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_subdirectory(app)
add_custom_target(run
COMMENT "Running GreeterApp..."
COMMAND GreeterApp
)
Behind the scenes, CMake is smart enough to understand that this means run
depends on GreeterApp
. As such, we don't need an add_dependencies()
call or DEPENDS
argument, although we could still add them if we prefer.
Either way, if we try to build this run
target whilst GreeterApp
hasn't been built or isn't up to date, CMake will first build it. From our build directory, we can test this using:
cmake --build . --target run
[1/2] Linking CXX executable app\GreeterApp.exe
[2/2] Running GreeterApp...
Hello World
Summary
Custom targets are the way to integrate any non-compilation task into your build workflow. They provide named entry points for developers to perform common utility actions.
- Phony Targets:
add_custom_target()
creates a target that runs commands but doesn't produce a file. It's the CMake equivalent of a phony target in aMakefile
. - Explicit Invocation: Custom targets are not built by default. A user must build them explicitly with
cmake --build . --target <name>
, making them ideal for optional utilities likeformat
ordocs
. - The
ALL
Keyword: AddingALL
to a custom target's definition makes it part of the default build, ensuring its commands run every time. - Using Build Presets: A build preset can specify which target(s) it builds using a
targets
array. - Explicit Dependencies: Use
add_dependencies()
to create a dependency between a normal target and a custom target, ensuring the custom target's commands run before the normal target is built.
Using Clang-Format
Using the Clang-Format tool to automatically enforce a consistent coding style, and integrating it into our build using a custom CMake target