Jak napisać skrypt cmake?

Konfiguracja CMake - wszystkie wpisy

W tym artykule pokażę jak napisać prosty skrypt cmake. Zrealizujemy najważniejsze zadania, jakich wymagamy od skryptu budowania:

  • Dodawanie plików źródłowych.
  • Określenie ścieżek include.
  • Określenie globalnych define’ów.
  • Dodanie bibliotek statycznych.
  • Dodanie flag kompilacji.

Dzięki skryptowi CMake możemy łatwo uruchamiać kompilację na różnych systemach operacyjnych, systemach budowania i generować projekty na różne IDE.

Minimalny skrypt

Minimalny skrypt cmake potrzebny do zbudowania projektu wygląda tak:

cmake_minimum_required(VERSION 3.10)

project(cmake_example_01)

add_executable(${CMAKE_PROJECT_NAME}
    main.c
)

Co tutaj się wydarzyło? Wykonaliśmy trzy operacje:

  1. Określiliśmy minimalną wymaganą wersję CMake – 3.10. CMake jest rozwijany od końca lat 90-tych i jego składnia ulegała zmianom. Najlepiej pisać nasze skrypty na wersję od 3.0 w górę. Kiedy dodamy tą linię nie musimy przejmować się wsparciem dla archaicznych wersji CMake.
  2. Określamy nazwę naszego projektu. W tym przykładzie jest to cmake_example_01. Ta nazwa jest później wykorzystywana w zmiennej ${CMAKE_PROJECT_NAME}, która służy nam do tworzenia plików wynikowych. Więcej o komendzie project możecie przeczytać w dokumentacji.
  3. Określamy plik wynikowy naszego projektu za pomocą komendy add executable. Jest to odpowiednik targetu z Makefile. Jako nazwę wykorzystaliśmy zmienną ${CMAKE_PROJECT_NAME}, a następnie dodaliśmy pliki źródłowe do skompilowania. W tym przypadku jest to tylko plik main.c, który zawiera prostą aplikację Hello World. Dla większych projektów oczywiście będziemy musieli podać więcej plików źródłowych.

Aby zbudować projekt najpierw musimy wybudować docelowe skrypty za pomocą CMake. Możemy to zrobić z konsoli:

cmake ../src -GNinja

Pierwszym argumentem komendy cmake jest ścieżka do naszego skryptu. Warto tu jeszcze wspomnieć, że domyślny skrypt CMake nazywa się CMakeLists.txt i to właśnie ten plik uruchomi powyższa komenda. Drugim argumentem jest generator. Wybrałem tutaj Ninja, ale równie dobrze może to być na przykład -G"Unix Makefiles", czy jakikolwiek inny ze wspieranych generatorów.

Warto tu również wspomnieć o spacjach w ścieżkach projektu. Nie od dziś wiadomo, że skrypty nie radzą sobie dobrze ze spacjami w ścieżkach. Dlatego najlepiej po prostu ich unikać. W moim przykładzie Ninja sobie poradził ze spacjami, ale Makefile już nie.

Mając wygenerowane skrypty ninja możemy odpalić kompilację:

ninja

W ten sposób utworzyliśmy plik cmake_example_01.exe, który następnie możemy uruchomić. Oczywiście jeżeli zamiast Ninja wybierzemy inny system budowania, komenda będzie się różnić.

Dodawanie headerów

Minimalny skrypt, który utworzyliśmy powyżej faktycznie działa, ale nie jest zbyt przydatny. Brakuje w nim jeszcze kilku rzeczy abyśmy mogli go używać na co dzień. A najważniejszą z nich jest dodawanie ścieżek include.

Możemy je dodać za pomocą komendy include_directories:

include_directories(
    inc
)

Powyższa komenda dodaje folder inc do ścieżek wyszukiwania headerów. Możemy po spacjach lub enterach (ale bez przecinków) dodawać kolejne foldery.

Te foldery zostaną dodane jako flagi do naszej komendy kompilacji (-I../inc):

[1/2] C:\Tools\MinGW\bin\gcc.exe -I../inc -MD -MT CMakeFiles/cmake_example_01.dir/main.c.obj -MF CMakeFiles\cmake_example_01.dir\main.c.obj.d -o CMakeFiles/cmake_example_01.dir/main.c.obj   -c ../main.c

Globalne definicje preprocesora

Definicje preprocesora dodaje się bardzo podobnie jak ścieżki include. Służy do tego komenda add_definitions:

add_definitions(
    -DEXAMPLE_DEFINE
)

Niestety w przeciwieństwie do include flagi kompilacji nie są automatycznie generowane z podanych nazw i trzeba samodzielnie podawać całą flagę. W przypadku kompilatora gcc ma ona postać -D<nazwa_define>.

Biblioteki statyczne

Aby dodać bibliotekę statyczną korzystamy z komendy target_link_libraries. Aby podlinkować standardową bibliotekę matematyczną musimy dodać komendę:

target_link_libraries(${CMAKE_PROJECT_NAME} m)

W efekcie do komendy linkowania dodana zostaje flaga -lm.

Możemy również skonfigurować ścieżki do własnych bibliotek prekompilowanych przy pomocy komendy link_directories.

link_directories(
    lib
)

W efekcie wymienione foldery będą dodane do kompilacji z flagą -L<pelna_sciezka_do_folderu>.

Flagi kompilacji

Jak widać opcje dodawania include czy bibliotek statycznych powodują wygenerowanie odpowiednich flag kompilacji. Możemy również dodać inne flagi korzystając z odpowiednich opcji CMake.

Aby dodać dowolne flagi kompilacji musimy je dodać do odpowiednich zmiennych. Flagi C przechowuje zmienna CMAKE_C_FLAGS, a ustawiamy ją komendą:

set(CMAKE_C_FLAGS "-Wall -Wextra -Og")

W tym przykładzie dodałem flagi warningów oraz optymalizację Og. Możemy również dodać flagi dla innych języków. Za flagi C++ odpowiada zmienna CMAKE_CXX_FLAGS. Po szczegóły dotyczące języków i flag odsyłam do dokumentacji CMake.

Możemy również ustawić flagi linkera:

set(CMAKE_EXE_LINKER_FLAGS "-Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections")

Powyższa linijka dodaje flagi linkera odpowiedzialne za generowanie pliku .map oraz usuwanie używanych sekcji.

Istnieją również inne zmienne, które wpływają na flagi kompilacji. Na przykład dotyczące standardu języka:

set(CMAKE_C_STANDARD 99)

Warto przejrzeć listę dostępnych opcji w dokumentacji CMake.

Ostateczna wersja skryptu rozszerzona o wszystkie wspomniane opcje wygląda tak:

cmake_minimum_required(VERSION 3.10)

project(cmake_example_01)

include_directories(
    inc
)

add_definitions(
    -DEXAMPLE_DEFINE
)

link_directories(
    lib
)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_FLAGS "-Wall -Wextra -Og")

set(CMAKE_EXE_LINKER_FLAGS "-Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections")

add_executable(${CMAKE_PROJECT_NAME}
    main.c
)

target_link_libraries(${CMAKE_PROJECT_NAME} m)

Jeżeli chcecie trochę poeksperymentować, na GitHubie znajdziecie użyty przeze mnie projekt z prostym hello worldem w C.

Podsumowanie

W tym artykule pokazałem najważniejsze elementy skryptu CMake, który może stanowić bazę do dalszej rozbudowy i nauki. Możemy na przykład dodać wsparcie dla cross kompilacji, które opisywałem w poprzednim poście. Możemy również utworzyć własne zmienne, aby lepiej pogrupować ścieżki, pliki i flagi używane w projekcie. Kiedy konfiguracja się rozrasta możemy również przenieść część skryptu do oddzielnych plików.

Konfiguracja CMake - Nawigacja

2 Comments

  1. Dobrą praktyką jest odwoływanie się do targetu gdy ustawiamy parametry projektu.
    I tak zamiast
    add_definitions(-DEXAMPLE_DEFINE)
    użylibyśmy:
    target_compile_definitions(mojTarget PUBLIC -DEXAMPLE_DEFINE)

    Pozwala to uniknąć problemów w przypadku gdy nasz projekt składa się z więcej niż jednej biblioteki.

    • GAndaLF

      7 marca 2021 at 13:03

      Dzięki Darek! Masz rację. Ten artykuł miał pokazać najprostszy skrypt, żeby nie wnikać za bardzo w szczegóły. Powstaną jeszcze kolejne części tej serii, gdzie chcę opisać podejście z targetami.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *