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.

Greg Filak
Published

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 a Makefile.
  • 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 like format or docs.
  • The ALL Keyword: Adding ALL 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.
Next Lesson
Lesson 44 of 47

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

Have a question about this lesson?
Answers are generated by AI models and may not be accurate