Snáď každý C/C++ programátor, ktorý napísal viac ako dvoj-súborové programy, bol nútený si napísať vlastný makefile a to niekedy nie je jednoduché. Cmake je riešenie ktoré sa snaží tuto činnosť podstatne zjednodušiť.
Určite mi dáte za pravdu, že kým jednoduché veci sa pomocou makefile dajú napísať celkom pohodlným spôsobom, tak komplikovanejšie a rozsiahlejšie projekty ktoré pozostávajú z množstva častí, ktoré sú na sebe rôzne závislé, sa stávajú nočnou morou. Čiastočne sa to snažili vyriešiť takzvané autotools, no na mňa to stále pôsobilo dojmom len menšieho zla. Ja osobne som často strávil množstvo hodín len tým, aby daný projekt bol zlinkovateľný a skompilovateľný klasickou cestou:
./configure
make
make install
Našťastie som nebol jediný kto takto rozmýšľal a tak vzniklo riešenie pod názvom cmake (http://www.cmake.org). Cmake je program na tvorbu make scriptov pre vaše projekty. Cmake vytvára na základe určitého predpisu makefile script obsahujúci pravidlá pre kompiláciu. Má dve hlavné výhody. Zjednodušuje proces písania a aj udržiavania makefile. Cmake sa snaží byť multiplatfomový (Cmake názov znamená cross-platform make). Skutočnosť, že ide o skutočne silný nástroj nasvedčuje aj to, že sa pre tento nástroj rozhodli aj samotní programátori KDE. Cmake okrem kompilačného postupu dokáže pripraviť prostredie, vytvárať inštaláciu vášho projektu, zvláda rôzne testovania, napríklad či je dostupné všetko potrebé pre kompiláciu a množstvo iných.
Pohľad programátora - Prvý kontakt s Cmake
Z Cmake programu budú mať najväčšiu radosť pravé programátori. Pozrime sa na to, ako to funguje. Základom pre Cmake je súbor CmakeLists.txt. Tento súbor obsahuje predpis podľa ktorého sa vygeneruje výsledný makefile. Tento súboru je dobre čitateľný a obsahuje iba to najnutnejšie. Poďme si teda napísať náš prvý CmakeLists.txt súbor.
Ukážeme si modelovú situáciu. Predstavme si že máme projekt ktorý sa skladá z main(hlavnej) časti my_project a z knižnice my_library. My_project, obsahuje main.cpp a project.cpp a my_library obsahuje common.cpp. My_project používa nami vytvorenú my_library knižnicu. Ja preferujem následnú štruktúru adresárov a súborov:
src/
src/my_project
src/my_project/main.cpp
src/my_project/project.cpp
src/my_library
src/my_library/common.cpp
Dôvod tejto štruktúry je jednoduchý. Umožňuje totiž jednoduché prirábanie nových súčastí projektu a taktiež umožňuje napríklad zobrať adresár my_library a použiť ho v inom projekte. Vráťme sa však k Cmake. Najprv si napíšeme cmakelists.txt pre knižnicu my_library:
src/my_library/CMakeLists.txt
PROJECT(MY_LIBRARY)
ADD_LIBRARY( my_library STATIC common.cpp )
INSTALL(TARGETS my_library DESTINATION ${CMAKE_INSTALL_PREFIX}/lib )
Na prvom riadku sa nachádza: PROJECT(my_library). Tento riadok len pomenúva projekt. Zaujímavejší je však ďalší riadok. Tam sa nachádza zoznam všetkých súborov, ktoré sa budú kompilovať do nášho projektu. Taktiež sa tu uvádza, že sa bude kompilovať aj statická knižnica. Tretí riadok určuje, kam sa výsledná knižnica bude kopírovať pri inštalácii. Na tento účel použijeme globálnu premennú, ktorá obsahuje kam sa celý projekt bude inštalovať. Premenné v Cmake sa používajú podobne ako pri programovaní v shell.
Momentálne už máme pripravenú kompiláciu a inštaláciu my_library. CMakeLists.txt bude vyzerať nasledovne:
src/my_project/CMakeLists.txt
PROJECT(MY_PROJECT)
//nastavime potrebne cesty kde sa bude hladat
INCLUDE_DIRECTORIES( ${MY_LIBRARY_SOURCE_DIR} )
LINK_DIRECTORIES( ${MY_LIBRARY_BINARY_DIR})
//nastavime linkovanie, zavislosti a kompilaciu
TARGET_LINK_LIBRARIES( app my_library )
ADD_EXECUTABLE( app main.cpp project.cpp )
ADD_DEPENDENCIES( app my_library )
//pri instalacii sa nakopiruje binarka
INSTALL( TARGETS app DESTINATION ${CMAKE_INSTALL_PREFIX}/bin )
Script je o niečo dlhší a pôsobí aj zložitejšie, no postupne si krok po kroku vysvetlíme, o čo všetko sa Cmake postará. Na prvom riadku je pomenovanie aktuálneho projektu, ako sme mali u my_library. Dvojica príkazov INCLUDE_DIRECTORIES a LINK_DIRECTORIES nastavuje cesty, ktoré sa budú prehľadávať pre includovanie a linkovanie. Myslím že každému sú známe prepínače -I a -L prekladača GCC. Príkaz TARGET_LINK_LIBRARIES() obsahuje zoznam knižníc, ktoré je potrebne prilinkovať k našej aplikácii. V tomto prípade je to MY_LIBRARY knižnica. Príkaz ADD_EXECUTABLE nastaví názov výslednej binárky na app a obsahuje zoznam všetkých súborov, ktoré je potrebné skompilovať pre tento projekt. ADD_DEPENDENCIES nastavuje závislosť na inom projekte(konkrétne na uvedenom MY_LIBRARY). Posledný príkaz je INSTALL. Ten nastavuje kam sa výsledná aplikácia bude kopírovať pri inštalácii.
Nakoniec tieto dve kompilácie spojíme do jedného celku. Pre tento účel si vytvoríme ďalší CmakeLists.txt, no tento sa bude nachádzať priamo v src/.
src/CMakeLists.txt
PROJECT(PRJ)
ADD_SUBDIRECTORY(my_library)
ADD_SUBDIRECTORY(my_project)
Dvojica príkazov ADD_SUBDIRECTORY sa postará o to, že sa budú hľadať v týchto adresároch ďalšie súbory CmakeLists.txt. Súbor je ideálny napríklad tiež pre spoločné nastavenia premenných.
Pohľad užívateľa – kompilácia
Teraz sa pozrieme na Cmake z pohľadu užívateľa, ktorý náš projekt bude kompilovať. Táto časť nie je vôbec zložitá. Ide o známy kolotoč. Pre náš projekt je potrebné vytvoriť makefile. U automake sa o to staral známy ./cofigure script, ktorý pripravil kompiláciu a tiež nastavil napríklad rôzne prepínače kompilácie, či kompilovať ako debug alebo release, kam nainštalovať projekt atď.
Cmake nám ponúka dve možnosti:
- cmake CmakeLists.txt
- ccmake CmakeLists.txt
Prvý spôsob je vhodný pre prípravu makefile bez špeciálnej konfigurácie. Program po spustení otestuje prítomnosť všetkých závislostí a vytvorí makefile. Druhý spôsob je vhodnejší, ak si chceme kompiláciu upraviť(minimálne ak chceme zmeniť cestu kam sa nainštaluje projekt). Program ccmake spustí aplikáciu, ktorou môžeme meniť rôzne premenné. Keď sme všetko pomenili podľa svojich predstáv, stlačíme klávesu "c" a ccmake vygeneruje makefile zo zmenami. Stačia už len 2 známe kroky:
make
make install
Moduly a ďalšie možnosti Cmake
Uvedený príklad bol jednoduchý. Avšak v reálnej praxi sú často naše projekty závislé na mnoho ďalších knižniciach. Určite poznáte testy na prítomnosť potrebných knižníc, keď spustíte ./configure. Cmake myslel aj na to. V Cmake je možné vytvárať vlastné moduly ktoré budú zisťovať prítomnosť knižníc a nastavovať potrebné premenné ktoré sa potom použijú pri kompilácii. Cmake však obsahuje už množstvo dopredu naprogramovaných modulov pre takmer všetky bežné knižnice. Ukážeme si ilustračný príklad, kedy je náš projekt závislý ešte na JPEG knižnici. CMakeLists.txt nášho projektu mierne pozmeníme a priložíme kontrolu prítomnosti JPEG knižnice:
src/my_project/CMakeLists.txt
PROJECT(MY_PROJECT)
//vyhladame jpeg kniznicu
FindJPEG()
IF (NOT JPEG_FOUND)
MESSAGE( FATAL_ERROR “Jpeg library missing” )
ENDIF( NOT JPEG_FOUND)
//nastavime potrebne cesty kde sa bude hladat
INCLUDE_DIRECTORIES( ${MY_LIBRARY_SOURCE_DIR} ${JPEG_INCLUDE_DIR} )
LINK_DIRECTORIES( ${MY_LIBRARY_BINARY_DIR})
//nastavime linkovanie, zavyslosti a kompilaciu
TARGET_LINK_LIBRARIES( app my_library ${JPEG_LIBRARIES} )
ADD_EXECUTABLE( app main.cpp project.cpp )
ADD_DEPENDENCIES( app my_library )
//pri instalacii sa nakopiruje binarka
INSTALL( TARGETS app DESTINATION ${CMAKE_INSTALL_PREFIX}/bin )
Za riadkom PROJECT() nasleduje volanie funkcie FindJPEG(). Funkcia je súčasťou modulu JPEG a otestuje prítomnosť JPEG knižnice. Ak knižnica existuje, funkcia nastaví JPEG_FOUND premennú na TRUE a do premenných JPEG_LIBRARIES vloží zoznam knižníc. Do JPEG_INCLUDE_DIR vloží cestu k hlavičkovým súborom knižnice. Ak sa JPEG knižnica nenájde, žiadna z premenných sa nevytvorí. Za volaním funkcie nasleduje vyhodnocujúci IF. V prípade že knižnica nebola nájdená, Cmake sa ukončí chybovým výpisom a makefile súbory nebudú vygenerované. Viac informácií už o pripravených moduloch sa dá zistiť pomocou man cmake v sekcii MODULES. Taktiež je možné si napísať vlastný modul, no to je však téma na celý seriál.
Ako na configure.h?
Predstavte si že určité nastavenie chceme preniesť priamo do nášho kódu. V C/C++ sa na to používa configure.h. Ukážeme si teda ako dostať nastavenia z Cmake do súboru configure.h. V našom projekte budeme používať makrá DEBUG_MESSAGES a ARRAY_SIZE. Prvé makro sa testuje či je definované a ak áno, v našom projekte sa zapnú debug hlásenia. Ďalšie makro obsahuje veľkosť nejakého poľa. Tieto dva makrá máme definované v configure.h. Pre tento účel má CMake funkciu CONFIGURE_FILE() ktorá zoberie šablónu a dosadením hodnôt vygeneruje náš configure.h. Vytvoríme si šablónu a nazveme ju configure.cmake:
src/my_project/configure.cmake
#if !defined(_CONFIGURE_H_)
#define _CONFIGURE_H_
#cmakedefine DEBUG_MESSAGES
#define ARRATY_SIZE @ARRAY_SIZE@
#endif
Ešte je nutné doplniť CONFIGURE_FILE a vytvorenie premenných:
src/my_project/CMakeLists.txt
PROJECT(MY_PROJECT)
//vytvorime cache premenne
OPTION(DEBUG_MESSAGES “enable/disable debug messages” OFF)
SET(ARRAT_SIZE “20” CACHE STRING “size of array”)
//vyhladame jpeg kniznicu
FindJPEG()
IF (NOT JPEG_FOUND)
MESSAGE( FATAL_ERROR “Jpeg library missing” )
ENDIF( NOT JPEG_FOUND)
//vygenerovanie configure.h
CONFIGURE_FILE( ${CMAKE_CURRENT_SOURCE_DIR}/configure.cmake
${CMAKE_CURRENT_SOURCE_DIR}/configure.h )
//nastavime potrebne cesty kde sa bude hladat
INCLUDE_DIRECTORIES( ${MY_LIBRARY_SOURCE_DIR} ${JPEG_INCLUDE_DIR} )
LINK_DIRECTORIES( ${MY_LIBRARY_BINARY_DIR})
//nastavime linkovanie, zavyslosti a kompilaciu
TARGET_LINK_LIBRARIES( app my_library ${JPEG_LIBRARIES} )
ADD_EXECUTABLE( app main.cpp project.cpp )
ADD_DEPENDENCIES( app my_library )
//pri instalacii sa nakopiruje binarka
INSTALL( TARGETS app DESTINATION ${CMAKE_INSTALL_PREFIX}/bin )
Na začiatku som použil dvojicu príkazov OPTION a SET.OPTION vytvára premennú, ktorá bude nadobúdať len hodnoty ON a OFF. Táto forma je vhodná len pre prepínače. Akonáhle človek chce nejaké číselné alebo textové nastavenie, je nútený použiť SET ktorý inicializuje CACHE premennú(táto premenná bude zobrazená po spustení ccmake). V šablóne configure.cmake si všimnite makro #cmakedefine. Toto makro slúži pre cmake a funguje tak že ak je prepínač v stave ON, bude nahradený za #define nazov_prepinaca inak sa nepoužije. Ďalšou zvláštnosťou je @ARRAY_SIZE@. Týmto spôsobom je možné do výsledného súboru dostať akúkoľvek premennú z Cmake, čiže @ARRAY_SIZE@ bude nahradený hodnotou ktorá sa nachádza v premennej ${ARRAY_SIZE}.
Záver
Cieľom článku nebolo popísať čo všetko Cmake dokáže, ale len predviesť aké výhody Cmake má, lebo dokáže toho naozaj veľa. Rozpisovať všetky možnosti tohto silného nástroja by bolo na nekonečný seriál a bol by to len preklad materiálov z www.cmake.org. Dúfam že sa vám tento nastroj zapáčil, aspoň tak ako sa zapáčil mne, a že vám ušetrí veľa času a nervov.
Ešte jeden tip na záver. Či už Cmake, alebo automake, alebo dokonca samotná kompilácia často znečisťovali sources adresáre haldou nepotrebných súborov. Zvykne to občas prekážať, ak zdrojové kódy umiestňujete pod CVS alebo Subversion. Mne sa nie raz stalo že som do Subversion vložil aj okolité smeti ako skompilované *.o súbory atď. Vytvoril som si vedľa adresára project/src/ adresár project/tmp/ v ktorom akonáhle zadám cmake ../src/, ostane mi čistý kód a smeti sa pregenerujú do tmp adresára.