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

Greg Filak
Published

In the last few lessons, we've explored how to customize our build process with add_custom_command() and add_custom_target(). These tools let us integrate any external script or utility into our workflow, transforming our build system from a simple compiler-driver into a complete development automation engine.

This lesson provides a practical, real-world example of this concept: automated code formatting. Maintaining a consistent coding style across a project, especially with multiple developers, is crucial for readability and maintainability. Manually enforcing style rules is tedious and prone to arguments. A far better approach is to automate it.

We'll learn how to use Clang-Format, the industry-standard tool for C++ code formatting, and integrate it seamlessly into our project with a custom CMake target.

Installing clang-format

The clang-format tool is part of the LLVM toolchain, which also includes the Clang compiler. You may already have it installed - you can check by running the following command:

clang-format --version

If you see a version number, you're ready to go. If not, we've provided some installation guides for various environments below.

On Windows

The easiest way is to install the "LLVM tools" via the Visual Studio Installer.

  1. Run the Visual Studio Installer and click "Modify" on your installation.
  2. Go to the "Individual components" tab.
  3. Search for "Clang" and select "C++ Clang Compiler for Windows".

Alternatively, you can install the full LLVM toolchain from its official GitHub releases page. During installation, make sure to select the option to "Add LLVM to the system PATH".

The clang-format tool should now be available from the Developer Command Prompt or PowerShell for VS.

On MSYS2 UCRT64

From the UCRT64 terminal, you can install the MinGW version of the LLVM toolchain, which includes clang-format:

pacman -S mingw-w64-ucrt-x86_64-clang-tools-extra

On macOS

The simplest way is with Homebrew:

brew install clang-format

If you've installed the full LLVM toolchain (brew install llvm), clang-format is included.

On Linux (Debian/Ubuntu)

Use your distribution's package manager:

sudo apt install clang-format

Confirming Installation was Successful

After installation, open a new terminal and verify it's working by running:

clang-format --version

Creating a .clang-format File

The behavior of clang-format is controlled by a configuration file, by default named .clang-format, which you place in the root directory of your project. This file is shared among the team, so that everyone conforms to a similar style.

When we run clang-format, it automatically finds and uses the configuration from this file.

The file uses the YAML format. A common practice is to start with one of the built-in base styles (like LLVM, Google, Microsoft, or WebKit) and then override the specific rules you want to change.

Let's create a simple .clang-format file in our Greeter project's root directory. We'll base our style on the Google standard but change the indent width to 2 spaces.

.clang-format

# Inherit from the Google style guide
BasedOnStyle: Google
# Override the indent width
IndentWidth: 2

There are hundreds of options you can configure. The official documentation is the definitive guide, and online tools like clang-format configurator can help you generate a configuration interactively.

Using clang-format from the Command Line

Let's quickly see how the tool works. Let's make one of our source files messy by adding inconsistent spacing and indentation.

app/src/main.cpp

#include <iostream>

int main()
{ std::cout << "Hello world";
  return 0;
  }

From your project's root directory, you can run clang-format with the -i flag (for "in-place") to format a file:

clang-format -i app/src/main.cpp

If you now open app/src/main.cpp, you'll see that it has been magically reformatted to comply with the rules in your .clang-format file.

app/src/main.cpp

#include <iostream>

int main() {
  std::cout << "Hello world";
  return 0;
}

Running this command for every file is tedious. Let's automate it.

Finding External Tools with find_program()

Before we can create a custom target that runs clang-format, our CMakeLists.txt needs a reliable way to find the clang-format executable on the developer's machine.

Hardcoding a path like /usr/bin/clang-format is not portable and will fail on most systems.

The standard CMake command for this task is find_program(). It searches for an executable in standard system locations and stores the full path in a variable if it's found.

The find_program() Syntax

The basic syntax is straightforward:

find_program(<variable> <program_name>)

In this example, <variable> will be the name of the variable where the result will be stored. By convention, this is often an all-caps name ending in _EXE or _EXECUTABLE.

<program_name> is the name of the executable to search for. CMake is smart about this; you can just provide clang-format, and it will automatically look for clang-format.exe on Windows.

How it Works

The find_program() command searches a series of locations in a well-defined order, starting with standard system binary directories (like those in the PATH environment variable).

  • If the program is found: The <variable> you provided will be set to the full, absolute path of the executable.
  • If the program is NOT found: The <variable> will be set to a special value: <variable>-NOTFOUND. For example, if we searched for CLANG_FORMAT_EXE, the variable would become CLANG_FORMAT_EXE-NOTFOUND.

As we briefly covered in our variables introduction, values using this special *-NOTFOUND format are treated as falsy by CMake. This lets us easily check whether clang-format was found using an if() statement.

A Practical Example

Let's use this command to find clang-format and report its location. We can add this to our root CMakeLists.txt.

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(Greeter)

add_subdirectory(app)

# Find the clang-format executable
find_program(CLANG_FORMAT_EXE clang-format) 

# Check if it was found
if(CLANG_FORMAT_EXE) 
  message(STATUS "Found clang-format at ${CLANG_FORMAT_EXE}") 
else() 
  message(WARNING "clang-format not found") 
endif()

When we configure our project, this script will search for the tool. If it's in our PATH, we'll see a success message:

cmake ..
...
-- Found clang-format at D:/msys64/ucrt64/bin/clang-format.exe
...

If clang-format isn't installed or isn't in the PATH, CLANG_FORMAT_EXE will not be set to a valid path. The if() condition will evaluate to false, and we'll see our warning message instead. This allows us to gracefully handle the absence of optional tools.

The REQUIRED Keyword

If a program is required for our build to be functional, we can add the REQUIRED argument to our find_program() command:

find_program(CLANG_FORMAT_EXE clang-format REQUIRED)

This removes the need for an if() check - if clang-format isn't found on the build system, our script would now simply throw an error explaining that it's required.

For a program like clang-format, this is generally not recommended. Making it REQUIRED would mean that anyone using our project would need to have clang-format installed, even if they have no desire to use it. Perhaps they already have some other formatting tool, or are using an IDE that takes care of it for them.

Instead, we should set our script up so that, if clang-format isn't found, the custom target that requires it simply isn't available, but the build otherwise works as normal. We'll set that up next.

Integrating clang-format as a CMake Target

Our goal is to create a custom target named format that, when built, will run clang-format on all the C++ source files in our project.

We'll add this logic to our root CMakeLists.txt.

Step 1: Find the clang-format Executable

First, we need to find the clang-format executable as before:

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(Greeter)

add_subdirectory(app)

# Find the clang-format executable
find_program(CLANG_FORMAT_EXE clang-format)

This command searches for a program named clang-format in standard system locations (i.e., the directories in the PATH environment variable) and stores the full path to it in the CLANG_FORMAT_EXE variable.

Step 2: Gather All Source Files

Next, we need a list of all the .cpp files in our project. We can get this by inspecting the SOURCES property of our target, which we'll save to a variable called target_sources:

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_subdirectory(app)

find_program(CLANG_FORMAT_EXE clang-format)
get_target_property(target_sources GreeterApp SOURCES)

Step 3: Create the Custom Target

Finally, we create the format target. We'll add a check to ensure clang-format was actually found before creating the target. This means the target won't be available on systems where the tool isn't installed.

Then, we'll pass our list of source files to clang-format using the -i argument as before.

One additional consideration here is that our target_sources paths are relative to the /app directory rather than the project root. For example, the path will be /src/main.cpp rather than /app/src/main.cpp.

We'll see a few different ways to deal with this as we build out this target over the next two lessons. For now, a simple solution is to set /app as the target's WORKING_DIRECTORY:

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_subdirectory(app)

find_program(CLANG_FORMAT_EXE clang-format)
get_target_property(target_sources GreeterApp SOURCES)

if(CLANG_FORMAT_EXE) 
  add_custom_target(format 
    COMMENT "Formatting files with clang-format..." 
    COMMAND ${CLANG_FORMAT_EXE} -i ${target_sources} 
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/app
  ) 
  message(STATUS "Added 'format' target") 
else() 
  message(WARNING
    "clang-format not found - format target unavailable") 
endif()

Using Our format Target

After configuring our project, we now have a simple, memorable command for a common development task. We can configure our project from our build directory to confirm that our target was added:

cmake ..
...
-- Added 'format' target
...

We can then run it from the build directory:

cmake --build . --target format
[1/1] Formatting files with clang-format...

This will invoke our custom target, which in turn runs clang-format with the -i flag on the list of all source files we collected.

We can also create a build preset for this in the usual way:

CMakePresets.json

{
  "version": 3,
  "configurePresets": [{
    "name": "base",
    "binaryDir": "${sourceDir}/build"
  }],
  "buildPresets": [{
    "name": "format",
    "configurePreset": "base",
    "targets": ["format"]
  }]
}
cmake --build --preset format

Formatting Multiple Targets

Let's expand this target to integrate some of the techniques we've seen in previous lessons, as well as introduce some new additions.

In this example, we've re-added our greeter library, meaning we now have multiple targets we want to run clang-format on.

To accommodate this, we add a FORMAT_TARGETS variable to store the list of the targets. We then iterate over this list and append each target's source files to an ALL_SOURCES variable.

Instead of using the WORKING_DIRECTORY argument, we'll handle the relative path issue (/src/main.cpp vs /app/src/main.cpp) using a different technique.

  1. We'll iterate over each source in the target
  2. If the source is already using an absolute path, we'll add it to our ALL_SOURCES list without modification. CMake includes an if(IS_ABSOLUTE SomePath) helper for this.
  3. If the source is using a relative path, we'll prefix the target's source path before appending it to ALL_SOURCES
  4. We can get a target's source path (eg c:/my-project/app and c:/my-project/greeter) from the SOURCE_DIR property.

Our complete target might look something like this:

cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_subdirectory(app)
add_subdirectory(greeter)

find_program(CLANG_FORMAT_EXE clang-format)
set(FORMAT_TARGETS GreeterApp GreeterLib)
set(ALL_SOURCES)

foreach(target IN LISTS FORMAT_TARGETS)
  get_target_property(target_sources ${target} SOURCES)
  get_target_property(target_source_dir ${target} SOURCE_DIR)
  
  # Convert relative paths to absolute paths for this target
  foreach(source ${target_sources})
    if(IS_ABSOLUTE ${source})
      list(APPEND ALL_SOURCES ${source})
    else()
      list(APPEND ALL_SOURCES ${target_source_dir}/${source})
    endif()
  endforeach()
endforeach()

if(CLANG_FORMAT_EXE)
  add_custom_target(format
    COMMENT "Formatting files with clang-format..."
    COMMAND ${CLANG_FORMAT_EXE} -i ${ALL_SOURCES}
  )
  message(STATUS "Added 'format' target")
else()
  message(WARNING
    "clang-format not found - format target unavailable")
endif()

If you mess up the formatting of source files in both /app and /greeter, your format target should now fix both of them:

cmake --build --preset format
[1/1] Formatting files with clang-format...

Summary

Integrating developer utilities like clang-format into our build system can improve the developer experience of working on the project. It automates tedious tasks, ensures consistency, and makes life easier for everyone on the team.

  • Automate Formatting: clang-format is the standard tool for automatically enforcing a C++ coding style. Its behavior is controlled by a .clang-format file in your project root.
  • Find Your Tools: Use find_program() to locate external command-line tools in a portable way.
  • Create Utility Targets: Use add_custom_target() to create named, standalone targets for developer tasks.
  • Gather Your Files: We can use get_target_property() to collect the source files associated with a target.
Next Lesson
Lesson 45 of 47

Using File Sets

An introduction to file sets and the target_sources() command, allowing us to manage our header files in a more controlled way

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