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:
- 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. - 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 komendzieproject
możecie przeczytać w dokumentacji. - 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 plikmain.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.
5 marca 2021 at 20:35
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.
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.