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
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.
- Run the Visual Studio Installer and click "Modify" on your installation.
- Go to the "Individual components" tab.
- 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 forCLANG_FORMAT_EXE
, the variable would becomeCLANG_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.
- We'll iterate over each source in the target
- If the source is already using an absolute path, we'll add it to our
ALL_SOURCES
list without modification. CMake includes anif(IS_ABSOLUTE SomePath)
helper for this. - If the source is using a relative path, we'll prefix the target's source path before appending it to
ALL_SOURCES
- We can get a target's source path (eg
c:/my-project/app
andc:/my-project/greeter
) from theSOURCE_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.
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