Thursday, 22 November 2018

Integrating Cppcheck and CMake

In my previous post, I gave an introduction to the static analysis tool Cppcheck. In this post, I'm going to show how to integrate Cppcheck into the CMake build system to generate a cppcheck-analysis target for the make / ninja build systems.

CMake

CMake is a meta build system that can be used to generate files for various build systems, including make and ninja. I would recommend to have a basic knowledge of how CMake works before reading this post. For an introduction to CMake, you can read my CMake tutorial.

FindCppcheck

In CMake you can find dependencies using a find(ModuleName) command. This commands looks for a file FindModuleName.cmake and will parse this file to find the dependency and add it's requirements. For Cppcheck, you can add a FindCppcheck.cmake in a folder cmake/modules off the root of your project.

You should then add the following to your root CMakeLists.txt to tell it where to search for modules:

1
2
# Add a custom CMake Modules directory
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH})

The code for the FindCppcheck.cmake is below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# Locate cppcheck and add a cppcheck-analysis target
#
# This module defines
#  CPPCHECK_BIN, where to find cppcheck
#
# To help find the binary you can set CPPCHECK_ROOT_DIR to search a custom path
# Exported argumets include
#   CPPCHECK_FOUND, if false, do not try to link to cppcheck --- if (CPPCHECK_FOUND)
#  
#   CPPCHECK_THREADS_ARG - Number of threads to use (default -j4)
#   CPPCHECK_PROJECT_ARG - The project to use (compile_comands.json)
#   CPPCHECK_BUILD_DIR_ARG - The build output directory (default - ${PROJECT_BINARY_DIR}/analysis/cppcheck)
#   CPPCHECK_ERROR_EXITCODE_ARG - The exit code if an error is found (default --error-exitcode=1)
#   CPPCHECK_SUPPRESSIONS - A suppressiosn file to use (defaults to .cppcheck_suppressions)
#   CPPCHECK_EXITCODE_SUPPRESSIONS - An exitcode suppressions file to use (defaults to .cppcheck_exitcode_suppressions)
#   CPPCHECK_CHECKS_ARGS - The checks to run (defaults to --enable=warning)
#   CPPCHECK_OTHER_ARGS - Any other arguments
#   CPPCHECK_COMMAND - The full command to run the default cppcheck configuration
#   CPPCHECK_EXCLUDES - A list of files or folders to exclude from the scan. Must be the full path
#  
# if CPPCHECK_XML_OUTPUT is set to an output file name before calling this. CppCheck will create an xml file with that name
# find the cppcheck binary
 
# if custom path check there first
if(CPPCHECK_ROOT_DIR)
    find_program(CPPCHECK_BIN
        NAMES
        cppcheck
        PATHS
        "${CPPCHECK_ROOT_DIR}"
        NO_DEFAULT_PATH)
endif()
 
find_program(CPPCHECK_BIN NAMES cppcheck)
 
if(CPPCHECK_BIN)
    execute_process(COMMAND ${CPPCHECK_BIN} --version
                  OUTPUT_VARIABLE CPPCHECK_VERSION
                  ERROR_QUIET
                  OUTPUT_STRIP_TRAILING_WHITESPACE)
 
    set(CPPCHECK_THREADS_ARG "-j4" CACHE STRING "The number of threads to use")
    set(CPPCHECK_PROJECT_ARG "--project=${PROJECT_BINARY_DIR}/compile_commands.json")
    set(CPPCHECK_BUILD_DIR_ARG "--cppcheck-build-dir=${PROJECT_BINARY_DIR}/analysis/cppcheck" CACHE STRING "The build directory to use")
    # Don't show thise errors
    if(EXISTS "${CMAKE_SOURCE_DIR}/.cppcheck_suppressions")
        set(CPPCHECK_SUPPRESSIONS "--suppressions-list=${CMAKE_SOURCE_DIR}/.cppcheck_suppressions" CACHE STRING "The suppressions file to use")
    else()
        set(CPPCHECK_SUPPRESSIONS "" CACHE STRING "The suppressions file to use")
    endif()
 
    # Show these errors but don't fail the build
    # These are mainly going to be from the "warning" category that is enabled by default later
    if(EXISTS "${CMAKE_SOURCE_DIR}/.cppcheck_exitcode_suppressions")
        set(CPPCHECK_EXITCODE_SUPPRESSIONS "--exitcode-suppressions=${CMAKE_SOURCE_DIR}/.cppcheck_exitcode_suppressions" CACHE STRING "The exitcode suppressions file to use")
    else()
        set(CPPCHECK_EXITCODE_SUPPRESSIONS "" CACHE STRING "The exitcode suppressions file to use")
    endif()
 
    set(CPPCHECK_ERROR_EXITCODE_ARG "--error-exitcode=1" CACHE STRING "The exitcode to use if an error is found")
    set(CPPCHECK_CHECKS_ARGS "--enable=warning" CACHE STRING "Arguments for the checks to run")
    set(CPPCHECK_OTHER_ARGS "" CACHE STRING "Other arguments")
    set(_CPPCHECK_EXCLUDES)
 
    ## set exclude files and folders
    foreach(ex ${CPPCHECK_EXCLUDES})
        list(APPEND _CPPCHECK_EXCLUDES "-i${ex}")
    endforeach(ex)
 
    set(CPPCHECK_ALL_ARGS
        ${CPPCHECK_THREADS_ARG}
        ${CPPCHECK_PROJECT_ARG}
        ${CPPCHECK_BUILD_DIR_ARG}
        ${CPPCHECK_ERROR_EXITCODE_ARG}
        ${CPPCHECK_SUPPRESSIONS}
        ${CPPCHECK_EXITCODE_SUPPRESSIONS}
        ${CPPCHECK_CHECKS_ARGS}
        ${CPPCHECK_OTHER_ARGS}
        ${_CPPCHECK_EXCLUDES}
    )
 
    # run cppcheck command with optional xml output for CI system
    if(NOT CPPCHECK_XML_OUTPUT)
        set(CPPCHECK_COMMAND
            ${CPPCHECK_BIN}
            ${CPPCHECK_ALL_ARGS}
        )
    else()
        set(CPPCHECK_COMMAND
            ${CPPCHECK_BIN}
            ${CPPCHECK_ALL_ARGS}
            --xml
            --xml-version=2
            2> ${CPPCHECK_XML_OUTPUT})
    endif()
 
endif()
 
 
 
# handle the QUIETLY and REQUIRED arguments and set YAMLCPP_FOUND to TRUE if all listed variables are TRUE
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
    CPPCHECK
    DEFAULT_MSG
    CPPCHECK_BIN)
 
mark_as_advanced(
    CPPCHECK_BIN 
    CPPCHECK_THREADS_ARG
    CPPCHECK_PROJECT_ARG
    CPPCHECK_BUILD_DIR_ARG
    CPPCHECK_ERROR_EXITCODE_ARG
    CPPCHECK_SUPPRESSIONS
    CPPCHECK_EXITCODE_SUPPRESSIONS
    CPPCHECK_CHECKS_ARGS
    CPPCHECK_EXCLUDES
    CPPCHECK_OTHER_ARGS)
 
# If found add a cppcheck-analysis target
if(CPPCHECK_FOUND)
    file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/analysis/cppcheck)
    add_custom_target(cppcheck-analysis
        COMMAND ${CPPCHECK_COMMAND})
    message("cppcheck found. Use cppccheck-analysis targets to run it")
else()
    message("cppcheck not found. No cppccheck-analysis targets")
endif()

This file includes comments to describe the various sections, however, I'll include some common settings that you may wish to override before calling it.

Checks to enable

To change the level of enabled checks you can use the CPPCHECK_CHECKS_ARGS variable. By default this is set to --enable=warning. You can override it to any of the supported check levels in Cppcheck.

Suppression

As discussed in my previous post you can suppress some warnings in Cppcheck using a suppression file. By default the above script will look for a suppressions file in .cppcheck_suppressions. If it exists it will add this to the command when calling cppcheck. To override this file you can change the CPPCHECK_SUPPRESSIONS argument.

1
`set(CPPCHECK_SUPPRESSIONS ${PROJECT_ROOT_DIR}/my_suppressions)

exitcode

When Cppcheck finds errors you can tell it what error code to use when exiting the program. With the above script the default exitcode is 1. To override this you can use the CPPCHECK_ERROR_EXITCODE_ARG. To set it to use the cppcheck default

1
set(CPPCHECK_ERROR_EXITCODE_ARG "")

XML output

Cppcheck supports outputting the results via XML. This can be helpful for CI systems to read the output and add a report. To enable XML output you can set the CPPCHECK_XML_OUTPUT variable to the file you want to use:

1
set(CPPCHECK_XML_OUTPUT "${PROJECT_BINARY_DIR}/analysis/cppcheck/cppcheck_analysis.xml")

Excluding files

If you have vendored third party code in your repository then you may not want to include that in your Cppcheck analysis. To exclude a file or folder you can create a list CPPCHECK_EXCLUDES

1
2
3
4
set(CPPCHECK_EXCLUDES
    ${CMAKE_SOURCE_DIR}/3rd_party
    ${CMAKE_BINARY_DIR}/
)

Calling FindCppcheck

To call cppcheck you might add something similar to the below snipped to your root CMakeLists.txt:

1
2
3
4
5
6
7
8
set(CPPCHECK_XML_OUTPUT "${PROJECT_BINARY_DIR}/analysis/cppcheck/cppcheck_analysis.xml")
 
set(CPPCHECK_EXCLUDES
    ${CMAKE_SOURCE_DIR}/3rd_party
    ${CMAKE_BINARY_DIR}/
)
 
find(Cppcheck)

This will then add the target make cppcheck-analysis to your build system. By default this target will fail your build if there are any static analysis errors.

Conclusion

In this post, I've described how you can integrated Cppcheck into your build system using CMake. This allows you to provide a simplified interface for all developers and your CI system to easily call Cppcheck as they would any other build target.

The new cppcheck-analysis target will allow for new developers to quickly call Cppcheck and also supports incremental static-analysis to encourage calling it on larger projects.

No comments:

Post a Comment