Custom Commands and Targets

Introduction

CMake allows you to define custom commands and custom targets to perform tasks that aren’t part of the standard build process. For example:

  • Generating source files from a script.
  • Running a code formatter or linter.
  • Copying files or running post-build steps.

Key Concepts

  • add_custom_command(): Defines a command to run at a specific stage of the build (e.g., before or after building a target).
  • add_custom_target(): Creates a new target that can be invoked manually (e.g., make my_target) and can depend on other targets or commands.
  • OUTPUT and DEPENDS: Used to specify files generated by the command and dependencies for the command.

Code Sample

  1. Let's start by creating makehello.cpp which will generate a simple function for us.
// This will generate a single function called sayHello()
// which prints Hello, World! to the stdin.
#include <fstream>
#include <iostream>

int main(int argc, char *argv[]) {
  // make sure we have enough arguments
  if (argc < 2) {
    return 1;
  }

  std::ofstream fout(argv[1], std::ios_base::out);
  const bool fileOpen = fout.is_open();
  if (fileOpen) {
    fout << "#include <iostream>" << std::endl << std::endl;
    fout << "void sayHello() {" << std::endl;
    fout << "  std::cout << \"Hello, World!\" << std::endl;" << std::endl;
    fout << "};" << std::endl;
    fout.close();
  }
  return fileOpen ? 0 : 1; // return 0 if wrote the file
}
  1. Let's create a makehello.cmake file which will create a MakeHello executable from makehello.cpp and run it using the custom command feature to finally generate our hello.h file.
add_executable(MakeHello makehello.cpp)

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/hello.h
  COMMAND MakeHello ${CMAKE_CURRENT_BINARY_DIR}/hello.h
  DEPENDS MakeHello
  )

Notice above how OUTPUT specifies that hello.h will be generated in the bin directory (called build in our case). This helps CMake decide when to run this custom command - i.e. whenever something references hello.h

  1. Let's edit the main.cpp file to call the sayHello function which will be defined in hello.h when it is generated. Your editor will indicate that hello.h is missing for now.
#include "hello.h"
#include <iostream>

int main() {
  sayHello();
  return 0;
}
  1. Finally, let's edit the CMakeLists.txt file to include the makehello.cmake file.
cmake_minimum_required(VERSION 3.15)
project(HelloWorld)

# Include the custom command.
include(makehello.cmake)

# We add the dependency to hello.h
# This will automatically run our custom command.
add_executable(HelloWorld main.cpp ${CMAKE_CURRENT_BINARY_DIR}/hello.h)

# Since hello.h is generated in the build/bin directory
# we need to add it to the list of directories
# that will be looked at for header files
target_include_directories(HelloWorld PRIVATE
                             ${CMAKE_CURRENT_BINARY_DIR}
                             )
  1. Run cmake and build.
cmake ..
cmake --build .

You should find hello.h in the build directory and everything should compile nicely.

  1. Output:
./HelloWorld
Hello, World!

Quiz

What is the purpose of add_custom_command?

add_custom_command defines a command to run during the build process, such as generating files or running scripts.

What does the OUTPUT argument specify in add_custom_command?

The OUTPUT argument specifies the file(s) generated by the custom command.

How is add_custom_target different from add_custom_command?

add_custom_command defines a command tied to specific outputs, while add_custom_target creates a named target that can be manually invoked or included in the default build.