mirror of
https://github.com/ttroy50/cmake-examples.git
synced 2025-12-18 12:14:36 +03:00
refactor the clang format example and add test
This commit is contained in:
@@ -11,4 +11,5 @@ add_subdirectory(subproject1)
|
||||
add_subdirectory(subproject2)
|
||||
|
||||
set(CLANG_FORMAT_BIN_NAME clang-format-3.6)
|
||||
set(CLANG_FORMAT_EXCLUDE_PATTERNS "build/" ${CMAKE_BINARY_DIR})
|
||||
find_package(ClangFormat)
|
||||
|
||||
@@ -32,14 +32,15 @@ $ tree
|
||||
```
|
||||
|
||||
* link:CMakeLists.txt[] - Top level CMakeLists.txt
|
||||
* link:.clang-format[] - The file describing the stype guide
|
||||
* link:.clang-format[] - The file describing the style guide
|
||||
* link:cmake/modules/FindClangFormat.cmake[] - Script to find the clang-format binary
|
||||
* link:cmake/modules/clang-format.cmake[] - Script to setup the format targets
|
||||
* link:cmake/scripts/clang-format-check-changed[] - A helper script to check against changed files in git
|
||||
* link:cmake/scripts/clang-format-check-changed.py[] - A helper script to check against changed files in git
|
||||
* link:cmake/scripts/clang-format-check-changed.py[] - An old simplified version of the above helper script
|
||||
* link:subproject1/CMakeLists.txt[] - CMake commands for subproject 1
|
||||
* link:subproject1/main.cpp[] - source for a subproject with no errors
|
||||
* link:subproject1/main.cpp[] - source for a subproject with no style errors
|
||||
* link:subproject2/CMakeLists.txt[] - CMake commands for subproject 2
|
||||
* link:subproject2/main2.cpp[] - source for a subproject that includes errors
|
||||
* link:subproject2/main2.cpp[] - source for a subproject that includes style errors
|
||||
|
||||
# Requirements
|
||||
|
||||
@@ -47,16 +48,18 @@ To run this example you must have clang format tool installed. This can be insta
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ sudo apt-get install clang-format
|
||||
$ sudo apt-get install clang-format-3.6
|
||||
----
|
||||
|
||||
It will result in the tool being available as:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ clang-format
|
||||
$ clang-format-3.6
|
||||
----
|
||||
|
||||
If you install a different version, you can edit the `CLANG_FORMAT_BIN_NAME` variable in the root CMakeLists.txt
|
||||
|
||||
# Concepts
|
||||
|
||||
## clang-format
|
||||
@@ -95,10 +98,10 @@ The format target will find any C++ source files and in place modify them to mat
|
||||
|
||||
[source,cmake]
|
||||
----
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.cxx *.hxx *.hpp *.cc)
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.cxx *.hxx *.hpp *.cc *.ipp)
|
||||
|
||||
# Don't include some common build folders
|
||||
set(CLANG_FORMAT_EXCLUDE_PATTERNS ${CLANG_FORMAT_EXCLUDE_PATTERNS} "build/" "/CMakeFiles/")
|
||||
set(CLANG_FORMAT_EXCLUDE_PATTERNS ${CLANG_FORMAT_EXCLUDE_PATTERNS} "/CMakeFiles/" "cmake")
|
||||
|
||||
# get all project files file
|
||||
foreach (SOURCE_FILE ${ALL_SOURCE_FILES})
|
||||
@@ -112,7 +115,14 @@ endforeach ()
|
||||
----
|
||||
|
||||
This will find files matching the common C++ suffixes and then remove any that match some
|
||||
common CMake build directories.
|
||||
common CMake directories.
|
||||
|
||||
In the root `CMakeList.txt` we also exclude the build directory by adding the line
|
||||
|
||||
[source,cmake]
|
||||
----
|
||||
set(CLANG_FORMAT_EXCLUDE_PATTERNS "build/")
|
||||
----
|
||||
|
||||
## format-check
|
||||
|
||||
@@ -123,13 +133,20 @@ if any files don't match the clang-format style
|
||||
|
||||
This target will check the output of `git status` and scan the files to check if they match the style. This can be used by developers to make sure their changed files match the correct style.
|
||||
|
||||
In this example the actual check is done with a helper script +clang-format-check-changed+. This calls the following command to check files:
|
||||
In this example the actual check is done with a helper script +clang-format-check-changed.py+. This script will run `git status --porcelain --ignore-submodules`
|
||||
to get a list of changed files, match them against the allowed extensions from the above list, and finally remove any
|
||||
that match the exclude pattern from +CLANG_FORMAT_EXCLUDE_PATTERNS+. It will then run these files through clang-format and
|
||||
exit with an error if the files do not match the style.
|
||||
|
||||
An example call to the +clang-format-check-changed.py+ script is:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
git status --porcelain \
|
||||
| egrep '*\.cpp|*\.h|*\.cxx|*\.hxx|*\.hpp|*\.cc' \
|
||||
| awk -F " " '{print $NF}' \
|
||||
| xargs -r clang-format -style=file -output-replacements-xml \
|
||||
| grep "replacement offset" 2>&1 > /dev/null
|
||||
cmake/scripts/clang-format-check-changed.py --file-extensions ".cpp,*.cpp,*.h,*.cxx,*.hxx,*.hpp,*.cc,*.ipp" --exclude=build/ --exclude=/CMakeFiles/ --exclude=cmake --clang-format-bin /usr/bin/clang-format-3.6
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
This will include all changed files in your git repository that match the patterns. In this example repository this can include files that
|
||||
are part of different examples.
|
||||
====
|
||||
|
||||
@@ -6,7 +6,7 @@ if(NOT CLANG_FORMAT_BIN_NAME)
|
||||
endif()
|
||||
|
||||
# if custom path check there first
|
||||
if(CPPCHECK_ROOT_DIR)
|
||||
if(CLANG_FORMAT_ROOT_DIR)
|
||||
find_program(CLANG_FORMAT_BIN
|
||||
NAMES
|
||||
${CLANG_FORMAT_BIN_NAME}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# A CMake script to find all source files and setup clang-format targets for them
|
||||
|
||||
# Find all source files
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.cxx *.hxx *.hpp *.cc)
|
||||
set(CLANG_FORMAT_CXX_FILE_EXTENSIONS ${CLANG_FORMAT_CXX_FILE_EXTENSIONS} *.cpp *.h *.cxx *.hxx *.hpp *.cc *.ipp)
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES ${CLANG_FORMAT_CXX_FILE_EXTENSIONS})
|
||||
|
||||
# Don't include some common build folders
|
||||
set(CLANG_FORMAT_EXCLUDE_PATTERNS ${CLANG_FORMAT_EXCLUDE_PATTERNS} "main2.cpp" "build/" "/CMakeFiles/")
|
||||
set(CLANG_FORMAT_EXCLUDE_PATTERNS ${CLANG_FORMAT_EXCLUDE_PATTERNS} "/CMakeFiles/" "cmake")
|
||||
|
||||
# get all project files file
|
||||
foreach (SOURCE_FILE ${ALL_SOURCE_FILES})
|
||||
@@ -24,21 +25,38 @@ add_custom_target(format
|
||||
${ALL_SOURCE_FILES}
|
||||
)
|
||||
|
||||
|
||||
add_custom_target(format-check
|
||||
COMMENT "Checking clang-format changes"
|
||||
COMMAND ${CLANG_FORMAT_BIN}
|
||||
# Use ! to negate the result for correct output
|
||||
COMMAND !
|
||||
${CLANG_FORMAT_BIN}
|
||||
-style=file
|
||||
-output-replacements-xml
|
||||
${ALL_SOURCE_FILES}
|
||||
| grep "replacement offset" 2>&1 > /dev/null
|
||||
| grep -q "replacement offset"
|
||||
)
|
||||
|
||||
# Get the path to this file
|
||||
get_filename_component(_clangcheckpath ${CMAKE_CURRENT_LIST_FILE} PATH)
|
||||
# have at least one here by default
|
||||
set(CHANGED_FILE_EXTENSIONS ".cpp")
|
||||
foreach(EXTENSION ${CLANG_FORMAT_CXX_FILE_EXTENSIONS})
|
||||
set(CHANGED_FILE_EXTENSIONS "${CHANGED_FILE_EXTENSIONS},${EXTENSION}" )
|
||||
endforeach()
|
||||
|
||||
set(EXCLUDE_PATTERN_ARGS)
|
||||
foreach(EXCLUDE_PATTERN ${CLANG_FORMAT_EXCLUDE_PATTERNS})
|
||||
list(APPEND EXCLUDE_PATTERN_ARGS "--exclude=${EXCLUDE_PATTERN}")
|
||||
endforeach()
|
||||
|
||||
# call the script to chech changed files in git
|
||||
add_custom_target(format-check-changed
|
||||
COMMENT "Checking changed files in git"
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMAND ${_clangcheckpath}/../scripts/clang-format-check-changed ${CLANG_FORMAT_BIN}
|
||||
COMMAND ${_clangcheckpath}/../scripts/clang-format-check-changed.py
|
||||
--file-extensions \"${CHANGED_FILE_EXTENSIONS}\"
|
||||
${EXCLUDE_PATTERN_ARGS}
|
||||
--clang-format-bin ${CLANG_FORMAT_BIN}
|
||||
)
|
||||
|
||||
|
||||
163
04-static-analysis/clang-format/cmake/scripts/clang-format-check-changed.py
Executable file
163
04-static-analysis/clang-format/cmake/scripts/clang-format-check-changed.py
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
|
||||
def check_file(filename, excludes, extensions):
|
||||
"""
|
||||
Check if a file should be included in our check
|
||||
"""
|
||||
name, ext = os.path.splitext(filename)
|
||||
|
||||
if len(ext) > 0 and ext in extensions:
|
||||
if len(excludes) == 0:
|
||||
return True
|
||||
|
||||
for exclude in excludes:
|
||||
if exclude in filename:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_directory(directory, excludes, extensions):
|
||||
output = []
|
||||
|
||||
if len(excludes) > 0:
|
||||
for exclude in excludes:
|
||||
if exclude in directory:
|
||||
directory_excluded = False
|
||||
return output
|
||||
|
||||
for root, _, files in os.walk(directory):
|
||||
for file in files:
|
||||
filename = os.path.join(root, file)
|
||||
if check_file(filename, excludes, extensions):
|
||||
print("Will check file [{}]".format(filename))
|
||||
output.append(filename)
|
||||
return output
|
||||
|
||||
def get_git_root(git_bin):
|
||||
cmd = [git_bin, "rev-parse", "--show-toplevel"]
|
||||
try:
|
||||
return subprocess.check_output(cmd).strip()
|
||||
except subprocess.CalledProcessError, e:
|
||||
print("Error calling git [{}]".format(e))
|
||||
raise
|
||||
|
||||
def clean_git_filename(line):
|
||||
"""
|
||||
Takes a line from git status --porcelain and returns the filename
|
||||
"""
|
||||
file = None
|
||||
git_status = line[:2]
|
||||
# Not an exhaustive list of git status output but should
|
||||
# be enough for this case
|
||||
# check if this is a delete
|
||||
if 'D' in git_status:
|
||||
return None
|
||||
# ignored file
|
||||
if '!' in git_status:
|
||||
return None
|
||||
# Covers renamed files
|
||||
if '->' in line:
|
||||
file = line[3:].split('->')[-1].strip()
|
||||
else:
|
||||
file = line[3:].strip()
|
||||
|
||||
return file
|
||||
|
||||
|
||||
def get_changed_files(git_bin, excludes, file_extensions):
|
||||
"""
|
||||
Run git status and return the list of changed files
|
||||
"""
|
||||
extensions = file_extensions.split(",")
|
||||
# arguments coming from cmake will be *.xx. We want to remove the *
|
||||
for i, extension in enumerate(extensions):
|
||||
if extension[0] == '*':
|
||||
extensions[i] = extension[1:]
|
||||
|
||||
git_root = get_git_root(git_bin)
|
||||
|
||||
cmd = [git_bin, "status", "--porcelain", "--ignore-submodules"]
|
||||
print("git cmd = {}".format(cmd))
|
||||
output = []
|
||||
returncode = 0
|
||||
try:
|
||||
cmd_output = subprocess.check_output(cmd)
|
||||
for line in cmd_output.split('\n'):
|
||||
if len(line) > 0:
|
||||
file = clean_git_filename(line)
|
||||
if not file:
|
||||
continue
|
||||
file = os.path.join(git_root, file)
|
||||
|
||||
if file[-1] == "/":
|
||||
directory_files = check_directory(
|
||||
file, excludes, file_extensions)
|
||||
output = output + directory_files
|
||||
else:
|
||||
if check_file(file, excludes, file_extensions):
|
||||
print("Will check file [{}]".format(file))
|
||||
output.append(file)
|
||||
|
||||
except subprocess.CalledProcessError, e:
|
||||
print("Error calling git [{}]".format(e))
|
||||
returncode = e.returncode
|
||||
|
||||
return output, returncode
|
||||
|
||||
|
||||
def run_clang_format(clang_format_bin, changed_files):
|
||||
"""
|
||||
Run clang format on a list of files
|
||||
@return 0 if formatted correctly.
|
||||
"""
|
||||
if len(changed_files) == 0:
|
||||
return 0
|
||||
cmd = [clang_format_bin, "-style=file",
|
||||
"-output-replacements-xml"] + changed_files
|
||||
print("clang-format cmd = {}".format(cmd))
|
||||
try:
|
||||
cmd_output = subprocess.check_output(cmd)
|
||||
if "replacement offset" in cmd_output:
|
||||
print("ERROR: Changed files don't match format")
|
||||
return 1
|
||||
except subprocess.CalledProcessError, e:
|
||||
print("Error calling clang-format [{}]".format(e))
|
||||
return e.returncode
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cli():
|
||||
# global params
|
||||
parser = argparse.ArgumentParser(prog='clang-format-check-changed',
|
||||
description='Checks if files chagned in git match the .clang-format specification')
|
||||
parser.add_argument("--file-extensions", type=str,
|
||||
default=".cpp,.h,.cxx,.hxx,.hpp,.cc,.ipp",
|
||||
help="Comma seperated list of file extensions to check")
|
||||
parser.add_argument('--exclude', action='append', default=[],
|
||||
help='Will not match the files / directories with these in the name')
|
||||
parser.add_argument('--clang-format-bin', type=str, default="clang-format",
|
||||
help="The clang format binary")
|
||||
parser.add_argument('--git-bin', type=str, default="git",
|
||||
help="The git binary")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Run gcovr to get the .gcda files form .gcno
|
||||
changed_files, returncode = get_changed_files(
|
||||
args.git_bin, args.exclude, args.file_extensions)
|
||||
if returncode != 0:
|
||||
return returncode
|
||||
|
||||
return run_clang_format(args.clang_format_bin, changed_files)
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(cli())
|
||||
11
04-static-analysis/clang-format/run_test.sh
Executable file
11
04-static-analysis/clang-format/run_test.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
mkdir -p build && cd build && cmake .. && make format-check
|
||||
RET=$?
|
||||
echo "return code was ${RET}"
|
||||
if [ ${RET} == "0" ]; then
|
||||
echo "test failed. Expected format-check to fail"
|
||||
exit 1
|
||||
else
|
||||
echo "test success"
|
||||
exit 0
|
||||
fi
|
||||
Reference in New Issue
Block a user