Set Up System Programming Environment

The following CMake setup example is for a single project:

.
├── cmake
│   ├── coverage.cmake
│   ├── coverage-report.cmake
│   ├── docs.cmake
│   ├── FindDoctest.cmake
│   ├── FindEGL.cmake
│   ├── FindEigen.cmake
│   ├── FindGLEW.cmake
│   ├── FindGLFW.cmake
│   ├── FindNanoFlann.cmake
│   ├── FindOpenGL.cmake
│   ├── FindOpenMesh.cmake
│   ├── format-cpp.cmake
│   ├── glsl.cmake
│   ├── glsl-make-shaders.cmake
│   ├── lint-cpp-style.cmake
│   ├── lint-python-style.cmake
│   ├── macros.cmake
│   └── tidy-cpp.cmake
├── docs
│   ├── proto
│   │   └── system
│   │       └── clock.rst
│   ├── _static
│   ├── _templates
│   ├── conf.py
│   └── index.rst
├── include
│   └── proto
│       ├── dgp
│       │   └── tensor.hpp
│       └── system
│           ├── clock.hpp
│           └── log.hpp
├── resources
│   ├── data
│   │   └── point-correspondences.txt
│   ├── scripts
│   │   ├── clang_tidy_suppression.py
│   │   ├── cpp_include_directive.py
│   │   └── obj_to_profile_image.py
│   └── shaders
│       ├── common
│       │   └── projection.glsl
│       ├── comp
│       ├── frag
│       │   ├── rgb2yuv.frag
│       │   └── per-pixel-ray-cast.glsl
│       ├── geom
│       ├── tesc
│       ├── tese
│       └── vert
│           └── fullscreen-triangle.vert
├── src
│   ├── dgp
│   │   ├── tests
│   │   │   ├── main.cpp
│   │   │   └── tensor.cpp
│   │   ├── CMakeLists.txt
│   │   └── tensor.cpp
│   ├── system
│   │   ├── tests
│   │   │   └── game-loop.cpp
│   │   ├── clock.cpp
│   │   ├── CMakeLists.txt
│   │   └── log.cpp
│   ├── CMakeLists.txt
│   └── main.cpp
├── third-party
│   ├── include
│   │   ├── doctest
│   │   │   └── doctest.h
│   │   ├── EGL
│   │   │   ├── eglext.h
│   │   │   ├── egl.h
│   │   │   ├── eglplatform.h
│   │   │   └── khrplatform.h
│   │   └── GL
│   │       ├── glcorearb.h
│   │       ├── glext.h
│   │       ├── glxext.h
│   │       └── wglext.h
│   ├── cpplint.py
│   └── run-clang-tidy.py
└── CMakeLists.txt

Top Level Directory

cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR)

# must be declared before project()
find_program(ClangCompiler "clang")
if(ClangCompiler)
    set(CMAKE_C_COMPILER ${ClangCompiler})
endif()
unset(ClangCompiler CACHE)

find_program(ClangPlusPlusCompiler "clang++")
if(ClangPlusPlusCompiler)
    set(CMAKE_CXX_COMPILER ${ClangPlusPlusCompiler})
endif()
unset(ClangPlusPlusCompiler CACHE)

project(proto LANGUAGES C CXX)

if(MSVC)
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# execute only once at ${CMAKE_SOURCE_DIR}
option(BUILD_TESTING "Enable/Disable unit tests." ON)
if(BUILD_TESTING)
    enable_testing()
endif()

set(BUILD_SHARED_LIBS TRUE CACHE
    BOOL "If this value is on, makefiles will generate a shared library.")

list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
include(macros)

enable_code_hardening()
check_coverage(CoverageFlags)
check_sanitizer(SanitizerFlags)
check_compiler_settings(CoverageFlags SanitizerFlags)

find_package(Doctest REQUIRED)
find_package(OpenGL REQUIRED)
find_package(EGL REQUIRED)
#find_package(Eigen REQUIRED)
#find_package(OpenMesh REQUIRED)
#find_package(NanoFlann REQUIRED)
#find_package(GLEW REQUIRED)
#find_package(GLFW REQUIRED)
include_directories(SYSTEM ${DOCTEST_INCLUDE_DIR}
#                           ${EIGEN_INCLUDE_DIR}
#                           ${NANOFLANN_INCLUDE_DIR}
#                           ${GLEW_INCLUDE_DIR} ${GLFW_INCLUDE_DIR}
                           ${OPENGL_INCLUDE_DIR} ${EGL_INCLUDE_DIR})

include(docs)
include(coverage)
include(format-cpp)
include(glsl)
include(lint-cpp-style)
include(lint-python-style)
include(tidy-cpp)

include_directories(include)
add_subdirectory(src)

# each shared library will append its target to ${PROJECT_LIBRARIES}
define_coverage(PROJECT_LIBRARIES)
set(PROJECT_LIBRARIES ${PROJECT_LIBRARIES}
#                      ${OPENMESH_LIBRARIES}
#                      ${GLEW_LIBRARIES} ${GLFW_LIBRARIES}
                      ${OPENGL_LIBRARIES} ${EGL_LIBRARIES})

set(Mains src/headless_gl.cpp)
make_standalones(PROJECT_LIBRARIES ${Mains})

Macros

function(list2string TARGET DELIMITER)
    foreach(arg ${ARGN})
        if(tmp)
            set(tmp "${tmp}${DELIMITER}${arg}")
        else()
            set(tmp "${arg}")
        endif()
    endforeach()
    set(${TARGET} "${tmp}" PARENT_SCOPE)
endfunction(list2string)

function(make_standalones LIBS)
    foreach(file_path ${ARGN})
        get_filename_component(file_name ${file_path} NAME)
        string(REPLACE ".cpp" "" bin ${file_name})

        add_executable(${bin} ${file_path})
        foreach(lib ${PROJECT_LIBRARIES})
            target_link_libraries(${bin} ${lib})
        endforeach()
    endforeach()
endfunction(make_standalones)

macro(check_coverage FLAGS)
    option(CODE_COVERAGE "Enable/Disable code coverage instrumentation." OFF)
    if(CODE_COVERAGE)
        set(${FLAGS} -g -O0 -fprofile-instr-generate -fcoverage-mapping)
    else()
        set(${FLAGS} "")
    endif()
endmacro(check_coverage)

macro(check_sanitizer FLAGS)
    set(CLANG_SANITIZER "None" CACHE STRING
        "Choose the type of sanitizer, options are: None ASan MSan TSan")
    set(${FLAGS} -g -O1 -fno-optimize-sibling-calls -fno-omit-frame-pointer)
    if(CLANG_SANITIZER STREQUAL "ASan")
        set(${FLAGS} ${${FLAGS}} -fsanitize=undefined,integer,address)
    elseif(CLANG_SANITIZER STREQUAL "MSan")
        set(${FLAGS} ${${FLAGS}} -fsanitize=undefined,integer,memory)
    elseif(CLANG_SANITIZER STREQUAL "TSan")
        set(${FLAGS} ${${FLAGS}} -fsanitize=undefined,integer,thread)
    else()
        set(${FLAGS} "")
    endif()
endmacro(check_sanitizer)

function(enable_code_hardening)
    option(CODE_HARDENING "Enable/Disable secure code instrumentation." OFF)
    if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CODE_HARDENING)
#        add_definitions(-D_FORTIFY_SOURCE=2)

        set(CompilerFlags -fstack-protector
                          -flto -fvisibility=default -fsanitize=cfi
                          -fsanitize=safe-stack)

        # Use -fPIC and -fPIE for all targets by default, including static libs
        set(CMAKE_POSITION_INDEPENDENT_CODE ON PARENT_SCOPE)

        # CMake doesn't add "-pie" by default for executables
        set(LinkerFlags -pie -Wl,--strip-all
                        -Wl,-z,relro
                        -Wl,-z,now
                        -Wl,-z,nodlopen -Wl,-z,nodump -Wl,-z,noexecstack)
    endif()

    list2string(CompilerFlags " " ${CompilerFlags})
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CompilerFlags}" PARENT_SCOPE)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CompilerFlags}" PARENT_SCOPE)

    list2string(LinkerFlags " " ${LinkerFlags})
    set(CMAKE_EXE_LINKER_FLAGS
        "${CMAKE_EXE_LINKER_FLAGS} ${LinkerFlags}" PARENT_SCOPE)
endfunction(enable_code_hardening)

function(check_compiler_settings SANITIZER COVERAGE)
    if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
        set(Flags -std=c++1z -stdlib=libc++ -Werror -Weverything
                  -Wno-c++98-compat -Wno-c++98-compat-pedantic
#                  -Wno-format-nonliteral -Wno-unreachable-code
#                  -Wno-double-promotion -fno-exceptions
#                  -Wno-documentation-unknown-command
                  ${${COVERAGE}} ${${SANITIZER}})
    endif()

    list2string(Flags " " ${Flags})
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${Flags}" PARENT_SCOPE)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Flags}" PARENT_SCOPE)

    if(MSVC)
        # Useless warnings
        add_compile_options(/wd4138)

        # Disable warnings related to Eigen
        add_compile_options(/wd4996)

        # OpenMesh requires this definition
        add_definitions(-D_USE_MATH_DEFINES)
    endif()
endfunction(check_compiler_settings)

check_coverage

The proposed code coverage tool consists of llvm-cov and llvm-profdata, which can be installed via

sudo apt-get install llvm

llvm-cov also supports an output format put forth by gcov, which in turn can be visualized by lcov. If one prefers to use these tools with clang, then replace the existing -fprofile-instr-generate -fcoverage-mapping with -fprofile-arcs -ftest-coverage, which is equivalent to specifying --coverage.

check_sanitizer

ASan, MSan, and TSan are mutually exclusive and cannot be used together. The proposed configuration enables UBSan by default. Note that LSan only works on Linux and is enabled by default when using ASan. Furthermore, MSan requires an instrumented C++ standard library.

enable_code_hardening

_FORTIFY_SOURCE is not supported by Clang at the moment. SafeStack is the only instrumentation that works out of the box. The Control Flow Integrity (CFI) sanitizer assumes the LLVMgold plugin is already installed e.g. via

sudo apt-get install llvm-5.0-dev

The correct usage of CFI requires -fvisibility=hidden, but that will make the source code incompatible with MSVC.

Enabling ASLR requires hacking CMake a bit. To check whether the security hardening features have been enabled, use hardening-check to examine the ELF binary. Unfortunately, these bare minimum features are the only ones supported by Clang.

find_package

FindOpenGL.cmake and FindEGL.cmake are mandatory while FindGLEW.cmake and FindGLFW.cmake have become optional conveniences for OpenGL.

Documentation

The proposed docs setup is no different from a blog. Two extra things to take care of are:

  • conf.py should contain

    primary_domain='cpp'
    
  • index.rst should contain

    Indices and tables
    ==================
    
    * :ref:`genindex`
    * :ref:`modindex`
    * :ref:`search`
    

The reason behind this is to keep documentation separate from code. Code comments do not count as documentation. This separation enables full utilization of a documentation tool to design the artifact before writing any code. Note that the mantra of self-documentating code is also applicable during this design phase.

find_program(SPHINX_BUILD "sphinx-build")
if(SPHINX_BUILD)
    set(DocsSrcDir ${PROJECT_SOURCE_DIR}/docs)
    set(DocsDstDir ${PROJECT_BINARY_DIR}/docs)

    add_custom_target(docs
                      COMMAND ${SPHINX_BUILD} -b html
                              ${DocsSrcDir} ${DocsDstDir})
    set_property(DIRECTORY APPEND PROPERTY
                 ADDITIONAL_MAKE_CLEAN_FILES ${DocsDstDir})

    unset(DocsSrcDir)
    unset(DocsDstDir)
else()
    message(STATUS "docs require sphinx-build.")
endif()

Code Coverage and Tests

find_program(COV NAMES llvm-cov-5.0 llvm-cov)
find_program(PROFDATA NAMES llvm-profdata-5.0 llvm-profdata)

if(CODE_COVERAGE AND COV AND PROFDATA)
    add_custom_target(coverage)
elseif(CODE_COVERAGE AND BUILD_TESTING)
    message(STATUS "Code coverage requires llvm-cov and llvm-profdata.")
endif()

function(define_coverage MODULES)
    set(CoverageResultsDir ${PROJECT_BINARY_DIR}/coverage-results)
    set(MergedCoverageData merged-coverage.profdata)

    if(CODE_COVERAGE AND COV AND PROFDATA)
        foreach(module ${${MODULES}})
            set(TargetLibraries ${TargetLibraries} $<TARGET_FILE:${module}>)
        endforeach()
        list2string(TargetLibraries "," ${TargetLibraries})

        add_custom_command(TARGET coverage POST_BUILD
            COMMAND ${CMAKE_COMMAND}
                    -D COV=${COV}
                    -D PROFDATA=${PROFDATA}
                    -D ProfDataDir=${PROJECT_BINARY_DIR}
                    -D TargetLibraries=${TargetLibraries}
                    -D CoverageResultsDir=${CoverageResultsDir}
                    -D MergedCoverageData=${MergedCoverageData} -P
                    ${PROJECT_SOURCE_DIR}/cmake/coverage-report.cmake)

        set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES
                     ${CoverageResultsDir} ${MergedCoverageData})
    endif()
endfunction(define_coverage)

function(compile_test MODULE_NAME SOURCE LIBS EXTERNAL_LIBS)
    if(BUILD_TESTING)
        set(CoverageResultsDir ${PROJECT_BINARY_DIR}/coverage-results)

        set(test_binary test-${MODULE_NAME})
        add_executable(${test_binary} ${SOURCE})

        foreach(lib ${LIBS})
            add_dependencies(${test_binary} ${lib})
            target_link_libraries(${test_binary} ${lib})
        endforeach()

        foreach(lib ${EXTERNAL_LIBS})
            target_link_libraries(${test_binary} ${lib})
        endforeach()

        add_test(NAME "${MODULE_NAME}"
                 WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/resources
                 COMMAND ${test_binary})
    endif()

    if(BUILD_TESTING AND CODE_COVERAGE AND COV AND PROFDATA)
        set(raw_profile ${CMAKE_CURRENT_BINARY_DIR}/${test_binary}.profraw)
        set(prof_data ${CMAKE_CURRENT_BINARY_DIR}/${test_binary}.profdata)

        target_compile_options(${test_binary} PRIVATE
            -fprofile-instr-generate=${raw_profile})

        foreach(lib ${LIBS})
            set(BINS ${BINS} $<TARGET_FILE:${lib}>)
        endforeach()
 
        add_custom_command(TARGET ${test_binary} POST_BUILD
            WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/resources
            COMMAND ${test_binary}
            COMMAND ${PROFDATA} merge -sparse -o ${prof_data} ${raw_profile}
            COMMAND ${COV} show -format=html
                        -instr-profile=${prof_data} -object ${BINS}
                        -output-dir=${CoverageResultsDir}/${MODULE_NAME})
        add_dependencies(coverage ${test_binary})

        set_property(DIRECTORY APPEND PROPERTY
             ADDITIONAL_MAKE_CLEAN_FILES ${raw_profile} ${prof_data})
    endif()
endfunction(compile_test)

Since performance analysis tools are orthogonal to code coverage and tests, it will not be included in the build system.

define_coverage

This will compile the total code coverage over the specified library modules via coverage-report.cmake. The script invocation is needed in order to glob all the individual coverage reports. Note that default.profraw is the default file name unless LLVM_PROFILE_FILE=”<name>.profraw” or -fprofile-instr-generate=<name>.profraw is specified.

file(REMOVE ${MergedCoverageData})

file(GLOB_RECURSE IndexedProfiles ${ProfDataDir}/*.profdata)

execute_process(COMMAND ${PROFDATA} merge -sparse ${IndexedProfiles}
                -o ${MergedCoverageData})
execute_process(COMMAND ${COV} show
                -instr-profile ${MergedCoverageData} -object ${TargetLibraries}
                -format=html -output-dir=${CoverageResultsDir})

compile_tests

This will make the tests, possibly with code coverage instrumentation. The test source code are embedded in the doctest framework.

ClangFormat

clang-format is a tool that will automatically format C/C++/Java/JavaScript code. One should avoid globbing files because new files will not be added to the existing glob. The solution to recompute the glob is touch_nocreate.

file(GLOB_RECURSE CxxIncludeFiles ${PROJECT_SOURCE_DIR}/include/*.[ch]pp)
file(GLOB_RECURSE CxxSourceFiles ${PROJECT_SOURCE_DIR}/src/*.[ch]pp)
set(AllCxxSourceFiles ${CxxIncludeFiles} ${CxxSourceFiles})

find_program(CLANG_FORMAT NAMES clang-format-5.0 clang-format)
if(CLANG_FORMAT)
    add_custom_target(format-cpp
                      COMMAND ${CMAKE_COMMAND} -E touch_nocreate
                              ${CMAKE_CURRENT_LIST_FILE}
                      COMMAND ${CLANG_FORMAT} -i -style=google
                              ${AllCxxSourceFiles})
else()
    message(STATUS "format-cpp requires clang-format.")
endif()

unset(AllCxxSourceFiles)
unset(CxxIncludeFiles)
unset(CxxSourceFiles)

GLSL

Since GLSL is C-like, one can use clang-format to enforce a coding style.

set(SrcDir ${PROJECT_SOURCE_DIR}/resources)
set(DstDir ${PROJECT_BINARY_DIR}/resources)

set(ShaderSrcDir ${SrcDir}/shaders)
set(ShaderDstDir ${DstDir}/shaders)

set(PreprocessorScript ${SrcDir}/scripts/cpp_include_directive.py)

find_program(CLANG_FORMAT "clang-format")
find_program(PYTHON3 "python3")
if(CLANG_FORMAT AND PYTHON3)
    add_custom_target(glsl
                      COMMAND ${CMAKE_COMMAND}
                              -D CLANG_FORMAT=${CLANG_FORMAT}
                              -D ShaderSrcDir=${ShaderSrcDir}
                              -D ShaderDstDir=${ShaderDstDir}
                              -D PreprocessorScript=${PreprocessorScript}
                              -D PYTHON3=${PYTHON3} -P
                              ${CMAKE_CURRENT_LIST_DIR}/glsl-make-shaders.cmake)
    set_property(DIRECTORY APPEND PROPERTY
                 ADDITIONAL_MAKE_CLEAN_FILES ${ShaderDstDir})
else()
    message(STATUS "glsl requires clang-format and python3.")
endif()

unset(SrcDir)
unset(DstDir)
unset(ShaderSrcDir)
unset(ShaderDstDir)
unset(PreprocessorScript)

Once again, invoking the script glsl-make-shaders.cmake is needed to glob all the individual shaders. The current setup does not search for files with a .glsl extension.

file(REMOVE_RECURSE ${ShaderDstDir})

file(GLOB_RECURSE ShaderFiles ${ShaderSrcDir}/*)

if(${ShaderFiles})
    execute_process(COMMAND ${CLANG_FORMAT} -i -style=google ${ShaderFiles})
endif()

set(ComputeShaderExt comp)
set(VertexShaderExt vert)
set(TessellationControlShaderExt tesc)
set(TessellationEvaluationShaderExt tese)
set(GeometryShaderExt geom)
set(FragmentShaderExt frag)
set(AllShaderExtensions
    ${ComputeShaderExt}
    ${VertexShaderExt}
    ${TessellationControlShaderExt} ${TessellationEvaluationShaderExt}
    ${GeometryShaderExt}
    ${FragmentShaderExt})

foreach(ext ${AllShaderExtensions})
    file(GLOB glob ${ShaderSrcDir}/${ext}/*.${ext})

    file(MAKE_DIRECTORY ${ShaderDstDir}/${ext})

    foreach(shader ${glob})
        get_filename_component(file_name ${shader} NAME)
        execute_process(COMMAND ${PYTHON3} ${PreprocessorScript}
                        ${shader} ${ShaderDstDir}/${ext}/${file_name})
    endforeach()
endforeach()

GLSL does not support the include preprocessor directive, but it’s quite easy to implement one in Python.

"""Implements the include directive of cpp."""

import argparse
import logging
import sys

from pathlib import Path


def find_first_match(idirs, ifile):
    """Return the first file that matches or None."""
    for idir in idirs:
        _ = Path(idir).joinpath(ifile)
        if _.exists():
            return str(_.resolve())

    return None


def process_include(args, infile):
    """Recursively process include directive."""
    for line in infile:
        _ = line.strip()
        if _.startswith('#include'):
            include_file = _.split()[1]
            if include_file.startswith('<') and include_file.endswith('>'):
                # do not search in local file directory
                include_dirs = args.include_dirs
            else:
                # search local file directory first
                _ = [str(Path(infile.name).parent.resolve()),
                     str(Path(infile.name).parent.parent.resolve())]
                include_dirs = args.include_dirs + _

            # remove <> and ""
            include_file = include_file[1:-1]

            match = find_first_match(include_dirs, include_file)
            if match is None:
                _ = 'Cannot find {} while searching in {}.'
                logging.warning(_.format(include_file, include_dirs))
                continue

            logging.info('Including {}'.format(match))
            with open(match, 'r') as match_infile:
                process_include(args, match_infile)
            args.outfile.write('\n')
        else:
            args.outfile.write(line)


def main():
    """Imitate cpp include directive."""
    _ = main.__doc__
    parser = argparse.ArgumentParser(description=_)
    parser.add_argument('infile', nargs='?', default=sys.stdin,
                        type=argparse.FileType('r'),
                        help='input to run preprocessor on')
    parser.add_argument('outfile', nargs='?', default=sys.stdout,
                        type=argparse.FileType('w'),
                        help='destination to store generated outputs')
    _ = 'directories to be searched for include directive'
    parser.add_argument('-I', nargs='+', dest='include_dirs', metavar='',
                        default=[], help=_)
    args = parser.parse_args()
    process_include(args, args.infile)


if __name__ == "__main__":
    main()

C++ Lint

cpplint is an additional set of style checks that one can apply after clang-format.

set(StyleLinter ${PROJECT_SOURCE_DIR}/third-party/cpplint.py)
set(Filters -legal/copyright
            -build/c++11)
list2string(Filters "," ${Filters})

set(CxxIncludeDir ${PROJECT_SOURCE_DIR}/include)
set(CxxSourceDir ${PROJECT_SOURCE_DIR}/src)

find_program(PYTHON3 "python3")
if(PYTHON3)
    add_custom_target(lint-cpp-style
                      COMMAND ${PYTHON3} ${StyleLinter}
                              --filter=${Filters}
                              --root=${CxxIncludeDir}
                              --recursive ${CxxIncludeDir}
                      COMMAND ${PYTHON3} ${StyleLinter}
                              --filter=${Filters}
                              --root=${CxxSourceDir}
                              --recursive ${CxxSourceDir})
else()
    message(STATUS "lint-cpp-style style checker requires python3.")
endif()

unset(StyleLinter)
unset(Filters)
unset(CxxIncludeDir)
unset(CxxSourceDir)

Python Lint

pycodestyle and pydocstyle are mandatory when coding in Python.

set(ScriptsDir ${PROJECT_SOURCE_DIR}/resources/scripts)

find_program(PY_CODE_STYLE "pycodestyle" DOC "PEP 8 Style Checker")
find_program(PY_DOC_STYLE "pydocstyle" DOC "PEP 257 Style Checker")
if(PY_CODE_STYLE AND PY_DOC_STYLE)
    add_custom_target(lint-python-style
                      COMMAND ${PY_CODE_STYLE} ${ScriptsDir}
                      COMMAND ${PY_DOC_STYLE} ${ScriptsDir})
else()
    message(STATUS "lint-python-style requires pycodestyle and pydocstyle.")
endif()

unset(ScriptsDir)

ClangTidy

Clang Static Analyzer tries to find bugs without executing the program. It can be invoked via clang --analyze or the scan-build script. The former runs the static analyzer and generates text reports. The latter has the option to generate a nice looking html report.

clang-tidy is a front end to scan-build. Note that a static analyzer check is different from a clang-tidy check; it just so happens that clang-tidy implements the default suite of static analyzer checks. A custom clang-tidy check should be implemented using clang-check.

Prefer run-clang-tidy-5.0.py to clang-tidy-5.0 because the former will use multithreading while the latter is single-threaded. The following uses a modified version that accepts line filters, which can be generated using clang_tidy_suppression.py.

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(TidyChecks -google-readability-namespace-comments
                   -cppcoreguidelines-pro-type-reinterpret-cast
                   -cppcoreguidelines-pro-bounds-pointer-arithmetic
                   -cppcoreguidelines-pro-bounds-array-to-pointer-decay
                   -cppcoreguidelines-pro-bounds-constant-array-index)
else()
    set(TidyChecks -google-readability-namespace-comments
                   -cppcoreguidelines-pro-bounds-pointer-arithmetic
                   -cppcoreguidelines-pro-bounds-array-to-pointer-decay
                   -cppcoreguidelines-pro-bounds-constant-array-index)
endif()

list2string(ClangTidyChecks "," ${TidyChecks})

find_program(CLANG_TIDY NAMES run-clang-tidy.py
             PATHS ${PROJECT_SOURCE_DIR}/third-party
                   /usr/bin)
if(CLANG_TIDY)
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    file(READ ${PROJECT_BINARY_DIR}/line_filter.json LineFilter)
    add_custom_target(tidy-cpp
                      WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
                      COMMAND ${CLANG_TIDY}
                              -p ${PROJECT_BINARY_DIR}
                              -header-filter="^${PROJECT_SOURCE_DIR}/include"
                              -checks=*,-*llvm*,${ClangTidyChecks}
                              -line-filter=${LineFilter})
else()
    message(STATUS "tidy-cpp requires clang-tidy.")
endif()

Source Code Organization

The current layout is arbitrary and is merely one way to decompose a system into subsystems.

src/CMakeLists.txt determines the overall system decomposition.

set(ModuleName system)
set(Libs "")
set(ExternalLibs "")
add_subdirectory(${ModuleName})
list(APPEND Modules ${ModuleName})

set(ModuleName dgp)
set(Libs system)
set(ExternalLibs "")
add_subdirectory(${ModuleName})
list(APPEND Modules ${ModuleName})

set(PROJECT_LIBRARIES ${Modules} PARENT_SCOPE)

src/system/CMakeLists.txt generates the subsystem and the corresponding tests, both of which could depend on other subsystems.

set(LocalSource  clock.cpp
                 log.cpp)

add_library(${ModuleName} ${LocalSource})
set_target_properties(${ModuleName} PROPERTIES DEBUG_POSTFIX "d")

foreach(lib ${Libs})
    add_dependencies(${ModuleName} ${lib})
    target_link_libraries(${ModuleName} ${lib})
endforeach()

foreach(lib ${ExternalLibs})
    target_link_libraries(${ModuleName} ${lib})
endforeach()

set(LocalTestSource tests/main.cpp
                    tests/clock.cpp)
set(Libs ${Libs} ${ModuleName})
compile_test("${ModuleName}" "${LocalTestSource}" "${Libs}" "${ExternalLibs}")