Integrating Tools with add_custom_command()
Learn how to attach commands to specific points in a target's build lifecycle using add_custom_command(TARGET)
for tasks like post-build file copying.
So far, our build process has been focused entirely on one thing: compiling C++ code. But a real-world development workflow involves much more. We often need to run external tools, copy files, or execute scripts as part of the build. Some common examples include:
- Copying a final executable to a specific deployment directory.
- Running a script to sign a binary after it has been built.
- Generating a checksum for a release artifact.
This lesson introduces the add_custom_command()
command with its TARGET
signature. This is CMake's mechanism for attaching arbitrary actions to specific moments in a target's build lifecycle, allowing you to integrate any external tool or script into your build process.
Using the TARGET
Signature of add_custom_command()
We've previously seen how the OUTPUT
signature of add_custom_command()
is used to generate source files. The TARGET
signature serves a different purpose: it attaches a command to the lifecycle of an existing target. It doesn't create new files that other targets depend on; it simply runs a command at a specified time.
The basic syntax is:
add_custom_command(TARGET <name> <timing>
COMMAND ...
COMMENT "..."
)
- The
<name>
is the name of an existing target, like ourGreeterApp
. - The
<timing>
keyword lets us specify exactly when our command should run within the build process of our target. We'll walk through the available timings in the next section. - The
COMMAND
keyword lets us specify the action we want to take - The
COMMENT
keyword lets us output an optional message to our build log when the command is executed
The Timing Hooks
There are three primary timings we can use:
PRE_BUILD
: The command runs before any other build rule for the target is executed. This is useful for setup tasks, like generating files that the compiler will need. We walk through a practical example of this in the next lesson.PRE_LINK
: The command runs after all source files for the target have been compiled into object files, but before the final link step that creates the executable or library. This is a more niche option, useful for tasks that need to process object files.POST_BUILD
: This is the most common and useful timing. The command runs after the target has been successfully built. It's perfect for post-processing tasks like copying the final binary, running a code signing tool, or packaging assets.
Let's see these timings in action. Our command will use the basic echo
program available on almost all platforms to log out a message. We'll run echo
on each phase of our GreeterApp
build in app/CMakeLists.txt
:
Files
Let's test our changes. Note that these commands will only be executed if GreeterApp
needs to be built. If it already exists in our build directory and hasn't changed, it may not be recompiled, meaning our commands will not be executed.
To be sure our program gets built, we can run the build step from a clean build directory.
cmake --build .
The build log should show the order of operations, with our echo
output documenting each step:
PRE_BUILD: GreeterApp is about to be built
PRE_LINK: Sources compiled, about to link
POST_BUILD: GreeterApp has been built successfully
Portable Commands
The COMMAND
argument can run any executable program that is installed on our machine, and accessible from the command line.
However, quite often, the commands we need will be basic, built-in options that are included with most platforms. echo
is an example of this, but we could also copy files using cp
, for example.
However, there is a portability problem here. cp
exists on Linux and macOS, but Windows' equivalent is called copy
. Commands that exist on multiple platforms may also work slightly differently, perhaps requiring different arguments.
We already know a few ways to solve this - the most obvious being something like an if(WIN32)
check, but CMake provides a more elegant solution. The cmake
executable bundles a suite of commands that are commonly needed by build scripts, including portable implementations of things like echo
and cp
/copy
.
Rewriting one of our echo
examples to use the version of echo
implemented by cmake
would look like this:
add_custom_command(TARGET GreeterApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo
"POST_BUILD: GreeterApp has been built successfully."
)
${CMAKE_COMMAND}
is a variable that holds the absolute path to the cmake
executable itself.
The -E
flag (for "execute") tells CMake not to start a build, but to instead run one of its own built-in, cross-platform utility commands.
You can get a full list of available commands by running cmake -E help
, but some of the most useful are:
copy
: Copies files.make_directory
: Creates a directory.copy_directory
: Copies entire directories.copy_directory_if_different
: Copies only the content that has changed from one directory to anotherrename
: Renames a file or directory.remove
: Deletes files.
By using ${CMAKE_COMMAND} -E <command>
instead of a native shell command, we ensure our custom commands will work on any platform.
Practical Example: Post-Build File Copying
Let's implement a classic and highly useful scenario: automatically copying our final executable to some other directory after a successful build. This is a common requirement for gathering all the necessary artifacts for testing or packaging.
We'll add this logic to app/CMakeLists.txt
. First, let's create a variable to store our destination, and use a file()
command to ensure it exists:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
add_executable(GreeterApp src/main.cpp)
# Define the destination directory and ensure it exists
set(DEPLOY_DIR "${PROJECT_SOURCE_DIR}/deploy")
file(MAKE_DIRECTORY ${DEPLOY_DIR})
Next, we'll add our custom command. The cmake -E
mode includes a copy
command that we can use for this. Remember, we can get the output of a target using the <TARGET_FILE:...>
generator expression:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
add_executable(GreeterApp src/main.cpp)
set(DEPLOY_DIR "${PROJECT_SOURCE_DIR}/deploy")
file(MAKE_DIRECTORY ${DEPLOY_DIR})
add_custom_command(TARGET GreeterApp POST_BUILD
COMMENT "Copying executable to ${DEPLOY_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:GreeterApp>
${DEPLOY_DIR}
)
Let's review the key components of this script:
- We create a variable
DEPLOY_DIR
to hold our destination path. This avoids hardcoding the path multiple times and makes the script easier to read and modify. We usefile(MAKE_DIRECTORY)
to ensure this directory exists before we try to copy into it. - We use a
POST_BUILD
command because we want to act on the final, successfully built executable. - We add a
COMMENT
to explain what this command is doing. This comment may also get output into the build log - To copy a file, we use the portable
${CMAKE_COMMAND} -E copy
. - We use the
$<TARGET_FILE:GreeterApp>
generator expression. This is the only reliable way to get the path to the final executable. It correctly resolves toGreeterApp.exe
on Windows andGreeterApp
on Linux, and it works regardless of the build configuration or generator. - We provide our
DEPLOY_DIR
as the destination directory we want our output copied to.
Now, the next time we build our executable, it will be copied to the /deploy
directory in our project root.
cmake --build .
If you look in your project's root, you'll find a new deploy/
directory containing your GreeterApp
executable. You may also see our COMMENT
output to the build log:
Linking CXX executable GreeterApp
Copying executable to D:/Projects/cmake/deploy
Remember, running a build may not actually build our executable if it is already in our /build
folder and our build system thinks it is up-to-date. If our commands aren't working, try deleting the existing executable (or the entire build directory) first.
On some environments, the COMMENT
may not show by default unless we increase the output verbosity. If it is important that the message is visible, we can use the echo
technique from before. We can pass multiple COMMAND
arguments like this:
add_custom_command(TARGET GreeterApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo
"Copying executable to ${DEPLOY_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:GreeterApp>
${DEPLOY_DIR}
)
Comparing install()
vs. POST_BUILD
Copy
For more complex deployment and packaging scenarios, using the install()
workflow we covered in is generally recommended. It provides fine-grained control over the entire process. It also makes our program easier to package with CPack, which we'll cover later in the course.
However, for simple projects, or for simple tasks within a larger project, a POST_BUILD
custom command is easier, more direct, and perfectly appropriate.
Understanding the Limitations
There's an important aspect of add_custom_command(TARGET ...)
that often trips up beginners. If we run our build again, without changing anything, our commands may not be executed.
This is because a custom command attached to a target is considered part of that target's dependency graph. The build system is smart; it sees that GreeterApp
is already up-to-date (its source files haven't changed), so it doesn't rebuild it. And if the target isn't rebuilt, its associated pre- and post-build commands are not run.
This is usually the desired behavior. You don't want to re-copy a file every single time you build if nothing has changed.
But what if you have a task that you always want to be able to run on demand, regardless of any changes? That's the job of a custom target, which we'll cover in the next lesson.
Summary
The add_custom_command(TARGET ...)
command lets us integrate external scripts and tools into our build process, attaching them to the lifecycle of your existing targets.
- Three Lifecycle Hooks: You can run commands at
PRE_BUILD
,PRE_LINK
, orPOST_BUILD
stages.POST_BUILD
is the most common. - Portability: The
${CMAKE_COMMAND} -E <command>
approach provides portable implementations of common command line tasks, such as working with the file system. - Use Generator Expressions: Use
$<TARGET_FILE:TargetName>
to reliably get the path to a target's output file. - It Only Runs When the Target Rebuilds: A custom command attached to a target is part of that target's build. If the target is up-to-date, the command will not be executed.