Konfiguracja CMake - wszystkie wpisy

Języki takie jak C i C++ zakładają, że programista wie co robi i pozwalają mu na wiele. Są bardzo konserwatywne w zgłaszaniu błędów i warningów. Nieraz obraca się to przeciwko programiście, dlatego sami aktywujemy dodatkowe flagi warningów podczas kompilacji i używamy różnych narzędzi do analizy kodu.

Ale do narzędzi takich jak cppcheck czy clang-tidy musimy podać te same pliki źródłowe, include, define co w głównym buildzie. Konfiguracja jest dosyć trudna. Na szczęście system budowania może nam w tym pomóc. W końcu posiada wszystkie informacje przekazywane kompilatorowi. Dzisiaj zobaczymy jak skonfigurować analizę statyczną w CMake.

Clang scan-build

Zanim przejdziemy do sposobów ingerujących w skrypt budowania najpierw omówimy narzędzie, które nie wymaga żadnych zmian. Jest nim scan-build wchodzący w skład LLVM, czyli narzędzie do statycznej analizy powiązane z kompilatorem clang. Aby go użyć musimy najpierw zainstalować toolchain LLVM.

Aby uruchomić analizę statyczną musimy użyć komendy scan-build a następnie wpisać komendę kompilującą cały projekt. Na przykład:

scan-build ninja -v

Skrypt potrafi rozpoznać wywołania kompilatora gcc lub clang, przechwycić flagi i wykonać statyczną analizę.

Dzięki temu odpada nam konfiguracja wejść do analizy statycznej. Możemy więc od razu uruchomić domyślny zestaw sprawdzeń. Oczywiście każdy taki tool ma spore możliwości konfiguracji i przejrzenie dokumentacji oraz pobawienie się poszczególnymi flagami na pewno nas nie ominie, jeżeli chcemy to dobrze ustawić. Jednak najgorsza część – podanie plików projektu jest już zrobiona za nas.

Więcej o scan-build przeczytacie w artykule Chrisa Colemana o Clang w embedded.

Statyczna analiza w skrypcie CMake

CMake ma wbudowane wsparcie (ale dopiero po tym jak dane narzędzie zainstalujemy) dla różnych narzędzi do analizy kodu:

Mamy również flagę LINK_WHAT_YOU_USE, która jest realizowana za pomocą opcji linkera. Niestety z include-what-you-use będziemy mieć problem, jeżeli chcemy korzystać na Windowsie, bo nie ma gotowej binarki. Jakąś opcją w takim wypadku jest uruchamianie tego narzędzia tylko na serwerze Continuous Integration.

Tak samo mamy flagi CMAKE_<LANG>_<NAZWA_TOOLA> modyfikująca domyślne ustawienia. Przy początkowych testach pewnie ta flaga będzie lepsza. Docelowo, szczególnie jeżeli będziemy chcieli dawać różne konfiguracje dla różnych buildów – wtedy lepiej nie modyfikować ustawień globalnie.

Ok to teraz w jaki sposób te flagi CMake działają? Zobaczmy to sobie na przykładzie cppcheck:

set(CMAKE_CXX_CPPCHECK "cppcheck")

Ta linijka dodana do skryptu CMake aktywuje cppcheck dla plików C++ (mamy oddzielną zmienną CMAKE_C_CPPCHECK dla C), a w stringu podajemy instrukcję konsolową uruchamiającą analizę.

Możemy również podać argumenty dla cppchecka po średnikach:

 set(CMAKE_CXX_CPPCHECK "cppcheck;--enable=all;--force")

Albo podać tą stałą nie w skrypcie, tylko jako argument w linii komend dla cmake:

cmake .. -GNinja -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;--force" 

Analogicznie wygląda to dla innych tooli. Na przykład dla clang-tidy:

set(CMAKE_C_CLANG_TIDY "clang-tidy;"-checks=-*,cert-*,clang-analyzer-*")
cmake .. -GNinja -DCMAKE_C_CLANG_TIDY ="clang-tidy;"-checks=-*,cert-*,clang-analyzer-*" 

Więcej na ten temat znajdziecie na blogu twórców CMake.

Zaawansowana konfiguracja

No fajnie, ale podawanie dłuższej konfiguracji w jednym stringu w linii komend, albo w jednej zmiennej CMake nie wygląda na zbyt poręczne. Dlatego w praktyce lepiej będzie nieco rozbudować nasze wsparcie dla statycznej analizy.

set(CPPCHECK_CONFIG
	"--enable=all"
	"--inconclusive"
	"--force" 
	"--inline-suppr"
	"--output-file=cppcheck.out"
)

find_program(CMAKE_C_CPPCHECK NAMES cppcheck)
if (CMAKE_C_CPPCHECK)
    list(APPEND CMAKE_C_CPPCHECK ${CPPCHECK_CONFIG})
endif()

find_program(CMAKE_CXX_CPPCHECK NAMES cppcheck)
if (CMAKE_CXX_CPPCHECK)
    list(APPEND CMAKE_CXX_CPPCHECK ${CPPCHECK_CONFIG})
endif()

W powyższym fragmencie skryptu umieściłem flagi cppcheck w oddzielnej zmiennej CPPCHECK_CONFIG. Następnie sprawdziłem, czy jest dostępny cppcheck i dodałem swój config cppchecka dla C i C++.

Mój config między innymi włącza wszystkie checki, włącza ingorowanie sprawdzeń w kodzie i określa plik wyjściowy z listą znalezionych problemów. W swoim projekcie możesz potrzebować bardziej szczegółowej listy aktywnych/nieaktywnych checków, czy listy ignorowanych warningów.

W każdym razie teraz konfiguracja statycznej analizy jest łatwiejsza do utrzymania. Możemy również dodać podobną konfigurację dla innych narzędzi:

set(CLANG_TIDY_CONFIG
	"-checks=-*,cert-*,clang-analyzer-*,performance-*,portability-*,readability-*,bugprone-*,misc-*"
	"--export-fixes=clang-tidy.out"
)

find_program(CMAKE_C_CLANG_TIDY NAMES clang-tidy)
if (CMAKE_C_CLANG_TIDY)
    list(APPEND CMAKE_C_CLANG_TIDY ${CLANG_TIDY_CONFIG})
endif()

find_program(CMAKE_CXX_CLANG_TIDY NAMES clang-tidy)
if (CMAKE_CXX_CLANG_TIDY)
    list(APPEND CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_CONFIG})
endif()

W powyższym fragmencie ustawiłem dla clang-tidy różne grupy checków i plik wynikowy.

Przykładowa zawartość plików z raportami z analizy może wyglądać tak dla cppcheck:

src\unity.c:2026:17: style: The scope of the variable 'ptrf' can be reduced. [variableScope]
    const char* ptrf;
                ^
src\unity.c:1980:23: style: Variable 'lnext' is assigned a value that is never used. [unreadVariable]
    const char* lnext = lptr;
                      ^
src\unity.c:686:0: style: The function 'UnityAssertBits' is never used. [unusedFunction]

^
src\unity.c:1112:0: style: The function 'UnityAssertDoubleSpecial' is never used. [unusedFunction]

^
src\unity.c:1094:0: style: The function 'UnityAssertDoublesWithin' is never used. [unusedFunction]

a tak dla clang-tidy:

  - DiagnosticName:  readability-inconsistent-declaration-parameter-name
    DiagnosticMessage:
      Message:         'function ''UnityMessage'' has a definition with different parameter names'
      FilePath:        "/src/unity_internals.h"
      FileOffset:      21469
      Replacements:    []
    Notes:
      - Message:         the definition seen here
        FilePath:        "src\\unity.c"
        FileOffset:      59181
        Replacements:    []
      - Message:         'differing parameters are named here: (''message''), in definition: (''msg'')'
        FilePath:        "src/unity_internals.h"
        FileOffset:      21469
        Replacements:
          - FilePath:        "src/unity_internals.h"
            Offset:          21494
            Length:          7
            ReplacementText: msg
    Level:           Warning
    BuildDirectory:  "test\\cyclic_buffer_mock\\out"
  - DiagnosticName:  readability-magic-numbers
    DiagnosticMessage:
      Message:         '126 is a magic number; consider replacing it with a named constant'
      FilePath:        "src\\unity.c"
      FileOffset:      4753
      Replacements:    []
    Level:           Warning
    BuildDirectory:  "test\\cyclic_buffer_mock\\out"

Możemy się również pobawić sposobem wyświetlania tych danych. Czasem config pozwala na output html. Czasem chcemy to podpiąć pod jakiś plugin Jenkinsa, czasem mamy jeszcze jakieś inne opcje. W każdym razie każdy tool potrafi zwracać listę błędów w formie tekstowej i zwykle chcemy to później jakoś obrobić.

Statyczna analiza jako opcja

Po zmianach z poprzedniego rozdziału mamy już możliwość łatwego zmieniania opcji samych narzędzi do analizy statycznej. Zostaje jednak jeszcze jedna sprawa – nie zawsze chcemy ją odpalać. No bo w końcu analiza statyczna wydłuża build, kod developerski czasem zawiera tymczasowe instrukcje i chcemy mieć wybór kiedy budować z analizą, a kiedy nie.

Właśnie dlatego warto dodać do skryptu CMake opcje:

option(STATIC_ANALYSIS "Build with static analysis" OFF)

if (STATIC_ANALYSIS)
include(../../cmake/static-analysis.cmake)
endif()

Konfigurację poszczególnych tooli do analizy umieściłem w oddzielnym pliku cmake/static-analysis.cmake i otoczyłem jego dodanie ifem.

W głównym pliku CMakeLists.txt możemy umieścić taki fragment, a następnie utworzyć build z analizą statyczną dodając tą flagę w komendzie cmake:

cmake .. -GNinja -DSTATIC_ANALYSIS=ON

Z kolei jeżeli nie umieścimy tej flagi – przygotujemy build bez analizy statycznej.

Możemy również pójść dalej i zrobić oddzielne opcje dla każdego toola do analizy.

Przykładowy kod

Przykład użycia statycznej analizy w skryptach cmake dodałem do projektu, który używałem już w poprzednich odcinkach z tej serii. Znajdziecie go na moim GitHubie – tag dla tego artykułu static-analysis.

Projekty unit testów test/cyclic_buffer oraz test/cyclic_buffer_mock załączają teraz dodatkowy plik z konfiguracją analizy statycznej zgodną z tym artykułem. Plik konfiguracyjny to cmake/static-analysis.cmake.

Podsumowanie

Dzięki wsparciu narzędzia do budowania możemy dużo łatwiej uporać się z konfiguracją analizy statycznej. Wbudowane wsparcie dla niektórych narzędzi dodatkowo ułatwia nam robotę. Dzięki temu łatwiej sforsować początkową barierę, ponieważ w wielu projektach rezygnuje się z dodatkowych narzędzi do analizy ze względu na trudności z konfiguracją.

Istnieje jeszcze opcja skonfigurowania w CMake dowolnych innych narzędzi jako komendy wywoływane z konsoli z odpowiednimi argumentami. Możemy je dodatkowo opakować w funkcje i moduły do dołączenia. Dzięki temu nie ograniczamy się tylko do tych narzędzi, które oficjalnie wspiera CMake. Jednak tutaj konfiguracja może być już trochę trudniejsza.

Jeżeli interesuje Cię temat narzędzi – koniecznie zapisz się na mój newsletter. Otrzymasz darmowy dokument opisujący różne typy narzędzi. A przy okazji nie przegapisz kolejnych artykułów!

Źródła

Blog Kitware – Static checks with CMake

Better Firmware with LLVM/Clang – Chris Coleman

Clang-tidy

Cppcheck

Konfiguracja CMake - Nawigacja