W prawie każdym projekcie potrzebujemy przechowywać jakieś wartości, które zmieniamy w zależności od wersji projektu. Najbardziej oczywistym przykładem jest właśnie numer wersji. Ale czasem chcemy wyświetlać również commit id z gita, datę kompilacji, czy użytą wersję kompilatora. Nie muszę chyba dodawać, iż aktualizacja takich danych manualnie jest niezwykle uciążliwa, a czasem wręcz niemożliwa (jak dodać commit id bezpośrednio w kodzie bez modyfikowania bez modyfikowania go?). W większych projektach będziemy w tym celu używać dodatkowych skryptów. A CMake ma wbudowane wsparcie do tego typu operacji.
Generowanie i kompilacja
Zanim przejdziemy do rozwiązywania konkretnych problemów projektowych muszę wspomnieć o tym jak CMake przygotowuje skrypty budowania.
Za pomocą komendy cmake wykonujemy operację generowania skryptów budowania zgodnie z plikiem CMakeLists.txt i opcjami konfiguracyjnymi przekazanymi z linii komend czy GUI. Do tych opcji należą narzędzia do budowania (np. Makefile, ninja), czy omawiany wcześniej plik Toolchain.
Za pomocą raz wygenerowanych skryptów możemy później wielokrotnie uruchamiać kompilację projektu. Również o ile w plikach źródłowych wprowadziliśmy jakieś zmiany. Kolejne generowanie projektu jest potrzebne dopiero o ile zmodyfikujemy plik CMakeLists.txt na przykład dodając nowy plik źródłowy. Skrypt buildowania potrafi wykryć, kiedy wymagane jest ponowne wygenerowanie projektu i przed kompilacją uruchamia wtedy komendę cmake.
Dlaczego różnica między generowaniem i kompilacją jest taka ważna? Ponieważ mówi nam w którym momencie tworzone są nasze stałe konfiguracyjne! Skoro generowanie stałych na podstawie szablonów (do którego za chwilę przejdziemy) to opcja CMake, wartości będą się aktualizować w momencie generowania, a nie kompilacji.
Data i godzina kompilacji
Oznacza to, iż daty kompilacji CMake nam nie wygeneruje. Na szczęście zrobi to dla nas kompilator! Możemy w tym celu użyć makr __DATE__ i __TIME__, które są częścią standardu C i powinny być wspierane przez każdy kompilator. Działają one bardzo podobnie jak __FILE__ oraz __LINE__ :
printf("\n"); printf("Compilation date: "); printf(__DATE__); printf("\n"); printf("Compilation time: "); printf(__TIME__); printf("\n");Output programu u mnie wygląda tak:
Compilation date: Dec 27 2020 Compilation time: 23:09:59Wersja projektu
Z kolei przy aktualizacji wersji projektu CMake może nam już pomóc. Aby określić wersję projektu musimy podać ją w pliku CMakeLists.txt:
project(cmake_example_02 VERSION 1.0.1)Tak więc do nazwy projektu dodajemy opcjonalny argument z numerem wersji. Jednak aby ten numer wersji wykorzystać w kodzie potrzebujemy czegoś jeszcze – szablonu kodu, gdzie wstawimy odpowiednią wartość:
//version.h.in #define VERSION_STRING @PROJECT_VERSION@ #define VERSION_MAJOR @PROJECT_VERSION_MAJOR@ #define VERSION_MINOR @PROJECT_VERSION_MINOR@ #define VERSION_PATCH @PROJECT_VERSION_PATCH@Utworzyłem plik version.h.in i umieściłem tam następujące define’y. Jak widać ich wartości to zmienne CMake otoczone znacznikami @ z obu stron. W szablonie możemy wykorzystać dowolną zmienną CMake. Listę wszystkich znajdziesz na tej stronie dokumentacji. Możemy też deklarować własne zmienne dzięki komendy set().
Potrzebujemy jeszcze komendy w pliku CMakeLists.txt generującej header z szablonu:
configure_file(version.h.in inc/version.h)W ten sposób z pliku version.h.in znajdującego się w głównym folderze źródeł zostanie utworzony plik inc/version.h w folderze wynikowym CMake. Czyli tam, gdzie znajdziemy również skrypty kompilacji.
Aby ten header mógł być używany w projekcie trzeba dodać jeszcze ścieżkę include:
include_directories( ${CMAKE_BINARY_DIR}/inc)W tym celu posłużyłem się zmienną CMake wskazującą folder wynikowy.
Tak więc zbierając wszystko do kupy – mam plik main korzystający z version.h:
#include <stdio.h> #include "version.h" #define STRINGIFY1(a) #a #define STRINGIFY(a) STRINGIFY1(a) int main(void) { printf(STRINGIFY(VERSION_STRING)); printf("\n%d.%d.%d\n", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); return 0; }Mam plik szablonu version.h.in, gdzie deklaruje stałe zależne od CMake:
#pragma once #define VERSION_STRING @PROJECT_VERSION@ #define VERSION_MAJOR @PROJECT_VERSION_MAJOR@ #define VERSION_MINOR @PROJECT_VERSION_MINOR@ #define VERSION_PATCH @PROJECT_VERSION_PATCH@I mam plik CMakeLists.txt, gdzie określam wersję projektu i tworzę header z pliku konfiguracyjnego:
cmake_minimum_required(VERSION 3.10) project(cmake_example_02 VERSION 1.0.1) configure_file(version.h.in inc/version.h) include_directories( ${CMAKE_BINARY_DIR}/inc ) add_executable(${CMAKE_PROJECT_NAME} main.c )Po uruchomieniu programu otrzymamy następujący output:
1.0.1 1.0.1Wersja kompilatora
Oczywiście poza danymi o wersji możemy w ten sposób obsłużyć każdą inną zmienną dostępną w CMake. Pokażę teraz jak dodać informacje o kompilatorze.
Musimy po prostu rozszerzyć plik version.h.in o dodatkowe stałe:
#define COMPILER_ID @CMAKE_C_COMPILER_ID@ #define COMPILER @CMAKE_C_COMPILER@A następnie wykorzystać je w kodzie:
printf("\n"); printf("Compiler name: "); printf(STRINGIFY(COMPILER_ID)); printf("\n"); printf("Compiler path: "); printf(STRINGIFY(COMPILER)); printf("\n");W konfiguracji CMake nie musimy nic zmieniać.
Output programu u mnie wygląda tak:
Compiler name: GNU Compiler path: C:/Tools/MinGW/bin/gcc.exeDane z gita
Kiedy chcemy w podobny sposób uzyskać dane z gita musimy już się trochę bardziej nagimnastykować. Nie ma na to prostej komendy cmake. Musimy uruchomić z cmake komendę gita zwracającą nam odpowiednie dane, przechwycić output do zmiennej i podać ją do pliku konfiguracyjnego. Rozwiązanie, które tutaj pokażę opiera się na artykule Matta Keetera. o ile znacie lepsze rozwiązanie – będę wdzięczny za opisanie go w komentarzach.
Aby zapisać dane z gita posłużymy się następującymi linijkami w CMakeLists.txt:
execute_process( COMMAND git rev-parse HEAD OUTPUT_VARIABLE GIT_COMMIT) execute_process( COMMAND git rev-parse --abbrev-ref HEAD OUTPUT_VARIABLE GIT_BRANCH)W ten sposób powstały zmienne GIT_COMMIT i GIT_BRANCH, które teraz możemy użyć w version.h.in:
#define GIT_BRANCH @GIT_BRANCH@ #define GIT_COMMIT @GIT_COMMIT@A po dodaniu następującego kodu do maina:
printf("\n"); printf("Git commit ID: "); printf(STRINGIFY(GIT_COMMIT)); printf("\n"); printf("Git branch: "); printf(STRINGIFY(GIT_BRANCH)); printf("\n");Otrzymamy taki output programu:
Git commit ID: f9338cecb6e77953712c42770a2306de90dc8c20 Git branch: masterTo rozwiązanie jest proste i nie rozwiązuje kilku problemów. W linkowanym wyżej artykule możemy na przykład sprawdzić, czy kod zawiera jakieś zmiany od ostatniego commitu i sygnalizuje to dzięki znaku +. Wykorzystano do tego komendę git diff.
Poza tym moje rozwiązanie pobiera dane z gita w momencie generowania. A o ile potem dodamy nowe commity, czy zmienimy branch bez zmian w CMake – zmienne dotyczące gita nie zostaną zaktualizowane.
Tak więc o ile pracujemy z kodem na maszynie developerskiej i jesteśmy w trakcie modyfikacji – musimy uważać, bo dane się nie zaktualizują. Ale do builda releasowego, gdzie budujemy od zera i nic już nie ruszamy – powinno się sprawdzić.
Kod przykładów
Kod, który użyłem w tym artykule można znaleźć na moim GitHubie. W tym samym projekcie jest również kod do wcześniejszego artykułu o podstawach CMake. Dojdą tutaj również przykłady z kolejnych części serii o CMake.
Podsumowanie
Jak widać korzystanie z CMake do generowania stałych w kodzie nie jest takie trudne, a może być całkiem przydatne. Schody zaczynają się dopiero kiedy chcemy przechwytywać dane z innych aplikacji jak na przykład git. Korzystając z CMake do generowania stałych możemy rozwiązać odwieczny problem aktualizowania zmiennych w różnych miejscach i korzystania z różnych skryptów automatycznych w każdym projekcie. A zmienne, które w ten sposób uzyskamy idealnie nadają się do konsoli debugowej, raportów o błędach, czy wyświetlania w menu. Dlatego na pewno warto zainteresować się tym tematem.