U každého, kdo se pohybuje ve světě UNIXu, či programuje ve staticky kompilovaných jazycích, je více než pravděpodobné, že se musel setkat s kompilací programu/knihoven ze zdrojových kódů. Pokud se nejednalo o programy typu „Hello world!“, pak je prakticky jisté, že se musel setkat i s problémem, jak výsledné binární soubory sestavit. Dnes si představíme nástroj, který vám s tím může pomoci – Cross Platform Make.
Na začátek by se hodilo říct, jak probíhá kompilace takového menšího většího projektu, jelikož to bude potřeba pro další věci, o kterých se budu zmiňovat. Pokud je vám tento proces jasný, můžete rovnou skočit na sekci „Co je to CMake?“.
Pro začátek řekněme, že mám jednoduchý projekt helloworldovského typu,
který čte, co mu předáme na standardním vstupu, a tyto „příkazy“
vykonává. Jedním ze základních příkazů bude i joke
, který
vypíše náhodně vybraný vtip ze své „databáze“. Stáhněte si zdrojové kódy.
Struktura souborů vypadá nějak takto:
src/ command.h joke.cpp joke.h main.cpp message.cpp message.h
Přičemž v command.h
se nachází třída command
. Tu
dědí třídy joke
(hlavička v joke.h
a implementace v
joke.cpp
) a message
(message.h
,
message.cpp
). Dále main.cpp
inkluduje všechny
hlavičkové soubory. Tak, a teď co s tím?
Kdybychom na to šli od lesa a používali GCC, mohli bychom prostě napsat
g++ src/joke.cpp src/main.cpp src/message.cpp -o hello
, což by nám
vše zkompilovalo a slinkovalo a byli bychom vysmátí. A u takovéhle malé blbosti
by se to snad i vyplatilo. Avšak je potřeba si uvědomit, že kompilace je operace
procesorově a časově náročná, a proto se nevyplatí pokaždé všechno
kompilovat znovu. Je lepší zkompilovat jen to, co je potřeba, a slinkovat to s
objektovými soubory, které již byly zkompilovány dříve a jejichž zdrojové
soubory se od té doby nezměnily.
Programátoři si to uvědomili již hodně dávno, a tak v roce 1977 byl v
Bellových laboratořích napsán program jménem make
. Jeho
konfigurační soubor je tzv. Makefile
, jehož syntaxe (v kostce) je
takováto:
akce: závisí_na příkazy ...
Jak akce, tak její závislosti mohou být soubory nebo názvy jiných akcí.
Pokud jde o soubory, make
zjišťuje, jestli se změnily, a jestli ne,
tak příkazy pro tu kterou akci neprovádí – nemusí přeci, když se soubory
nezměnily.
Jak by vypadal Makefile
pro náš projekt?
hello: main.o joke.o message.o g++ main.o joke.o message.o -o hello main.o: src/main.cpp src/command.h src/message.h g++ src/main.cpp -c -o main.o message.o: src/message.cpp src/message.h src/command.h g++ src/message.cpp -c -o message.o joke.o: src/joke.cpp src/joke.h src/command.h g++ src/joke.cpp -c -o joke.o
Teď se opravdu bude kompilovat jen to, co bude potřeba.
Jenže, ač byl make
opravdu skvělý nástroj, začaly se objevovat
problémy. Některým se nelíbilo kupř. to, že příkazy musely být za každou
cenu odsazeny právě jedním tabulátorem – a pokud nebyly, make
vám
vysypal krásnou chybovou hlášku. Jiní zase říkali, že psaní
Makefiles
je pořád jedno a to samé, prostě rutina – a ono ano,
vždyť je to pořád jen o tom zkompilovat každý zdrojový soubor a poté je
všechny nějak slinkovat – do knihoven, spustitelného programu. Ale asi
nejhorší je, že ručně psané Makefiles
povětšinou nejsou moc
přenosné, protože každý operační systém může mít jinde umístěny
hlavičkové soubory, knihovny, ba i příkazy – binárky.
Proto vznikly různé projekty, které Makefiles
dokázaly generovat.
V UNIXovém světě je asi nejznámější GNU Autotools,
kteréžto pomocí různých maker vygenerují shellový skript
configure
. A ten po spuštění vygeneruje z různých koster soubory,
mezi nimi i Makefiles
. Odtud je známa „svatá trojice“:
./configure; make; make install
, pomocí níž se instalují projekty
využívající GNU Autotools.
Co je to CMake?
Teď se již můžeme dostat k samotnému CMake. Kdybyste někde viděli
srovnání s GNU Autotools, není to náhoda. Protože ve své podstatě
obojí dělá to samé. Hlavní rozdíl je však v tom, že CMake na rozdíl
od GNU Autotools umí nejen Makefiles
. Je to přeci Cross
Platform Make! Takže kromě Makefiles
umí generovat mimo jiné taky
projektové soubory Visual Studia, Kdevelopu3, Code::Blocks a dalších. A možnosti
začlenění dalších jsou díky jeho modulární architektuře prakticky
neomezené.
Taky negeneruje, jako GNU Autotools, shellový skript, ale je potřeba
mít na stroji jeho binárku, která čte konfigurační soubor
CMakeLists.txt
a z něj je generuje požadovaný typ výstupního
projektového souboru.
(Příklady v článku jsou testovány na CMake verze 2.6.3.)
Helloworldovský projekt a CMake
Jak by tedy vypadal CMakeLists.txt
pro náš menší větší
projekt? Naprosto jednoduše:
PROJECT(hello) ADD_EXECUTABLE(hello src/main.cpp src/joke.cpp src/message.cpp)
Ano, to je celé. Jak můžete vidět, abychom dosáhli stejného výsledku,
stačí napsat o mnoho méně řádek. Ale hlavní je, že tím získáváme i
možnost nechat CMake vygenerovat např. projektový soubor Visual Studia a
zkompilovat zdrojové kódy v něm – bez toho, abychom změnili jediné písmeno v
CMakeLists.txt
.
Soubor CMakeLists.txt
obsahuje volání ve tvaru
název(argumenty)
, kde argumenty
jsou řetězce
alfaumerických (plus nějakých dalších) znaků oddělené mezerami (pokud je
potřeba, aby nějaký argument obsahoval mezeru, prostě se jeho obsah obklopí
uvozovkami).
Zatím jsme mohli vidět dvě volání – a to PROJECT()
a
ADD_EXECUTABLE()
. (Názvy jsou case-insensitive.) PROJECT()
přijímá jako první argument název projektu. Jako další argumenty můžete
přidat programovací jazyky používané v projektu (viz dokumentace).
ADD_EXECUTABLE()
se předává nejdříve název cíle pro spustitelný soubor, který se má
vytvořit. Zbytek argumentů je seznam zdrojových kódů, ze kterých má být
spustitelný soubor sestaven.
Teď se již můžeme vrhnout na samotné sestavení. Pokud používáte
Windows, společně s CMake je distribuována grafická aplikace,
která umožňuje nakonfigurování sestavení a vygenerování potřebných
souborů. Pokud pracujete na UNIXu, CMake nabízí ncurses
rozhraní podobné tomu na Windows – pokud ve vašem systému má binárka
standardní název a je v $PATH
, stačí pouze spustit
ccmake
. Jinak vygenerování z příkazové řádky je asi takovéto
(předpokládejme, že jste ve složce, kde se nachází
CMakeLists.txt
):
$ mkdir build && cd build $ cmake ..
Jak lze vidět, nejdříve jsme vytvořili adresář build
, kam
půjdou vygenerované a zkompilované soubory. Jde o tzv. „out-of-source build“.
Pokud bychom nevytvářeli žádný buildovací adresář a spustili
cmake
přímo z adresáře se zdrojovými kódy, šlo by o tzv.
„in-source build“. Druhý způsob ale není moc doporučován, protože
CMake vytvoří v aktuální složce změť souborů, které byste pak
museli mazat růčo – akorát by se tím zaneřádily zdrojové kódy. V
závislosti na tom, co jste po CMake chtěli, aby vygeneroval, teď
můžete spustit sestavovací proces. Pokud byl cílem UNIXový
Makefile
, pak to bude:
$ make
Až make
doběhne, měl by být v adresáři build
spustitelný soubor hello
. Spustíme pomocí:
$ ./hello
A můžeme se kochat tím, jak nám všechno funguje (v horším případě nefunguje).
CMake kromě sestavení nabízí i možnost instalování zkompilovaných
(a i jiných) souborů. Slouží k tomu volání INSTALL()
v CMakeLists.txt
:
INSTALL(TARGETS hello RUNTIME DESTINATION bin )
Jako první toto volání přijímá, co vůbec chceme instalovat.
TARGETS
mu říká, že se jedná o cíle vytvořené v
CMakeLists.txt
(např. pomocí ADD_EXECUTABLE()
), mezi
další možnosti patří FILES
, což INSTALL()
řekne,
že chceme instalovat jednotlivé soubory, více viz dokumentace. Dalšími argumenty
je seznam právě těch cílů nebo souborů, kterých se tahle instalace má
týkat. INSTALL()
pak přijímá další argumenty, které upřesňují,
co se má s cíli / se soubory dělat. V našem případě specifikujeme, že pro
spustitelné programy (RUNTIME
) se má nastavit jejich umístění
(DESTINATION
) na adresář bin
. Adresář je počítán
relativně od CMAKE_INSTALL_PREFIX
. Na UNIXech je defaultně
nastaven na /usr/local
. Takže pokud bychom opět spustili v buildovací
složce CMake a poté řekli make
, ať sestaví cíl
install
:
$ cmake .. # make install
V /usr/local/bin
by se nám měl ocitnout spustitelný soubor
hello
. Pokud máte /usr/local
v $PATH
, pak
stačí napsat hello
a program bude spuštěn.
Pokud chcete, aby se soubory instalovaly jinam, stačí změnit
CMAKE_INSTALL_PREFIX
:
$ cmake -DCMAKE_INSTALL_PREFIX=/opt/hello .. # make install
Sestavujeme knihovny
Kromě sestavování binárek taky potřebujeme někdy udělat knihovnu, kterou
budou moci použít ostatní knihovny/programy. Kdyby toto CMake
nepodporoval, nebyl by moc užitečný. Řekněme, že jsme napsali nějakou opravdu
skvělou knihovnu umožňující načtení celého souboru do vector
u
řádek, kteréžto jsou následně seřazeny, a chceme ji zpřístupnit ostatním
– aby se mohli kochat naším krásným kódem a my ukájet nad tím, jak
pomáháme open-source komunitě. Stáhněte si zdrojové
kódy.
include/ quicksort.h file_sort.h src/ file_sort.cpp test/ abcd.txt basic.cpp quicksort.cpp
V adresáři include
se nacházejí hlavičkové soubory, které by
se měly instalovat a jsou využívány soubory se zdrojovými kódy v knihovně. V
src
je jediný soubor – file_sort.cpp
– s vlastní
implementací celé naší knihovny (celá naše knihovna se skládá z jedné
funkce). A taková knihovna by samozřejmě měla být s dávkou testů, proto je
máme i my, v adresáři test
. quicksort.cpp
testuje
funkčnost našeho řadicího algoritmu, basic.cpp
zase zkouší, jestli naše
knihovní funkce file_sort()
dělá, co by měla. Tentokráte bude
CMakeLists.txt
trochu delší, proto si ho rozeberme postupně:
PROJECT(file_sort) MACRO(CREATE_LIBRARY NAME) ADD_LIBRARY(${NAME} SHARED ${ARGN}) SET_TARGET_PROPERTIES(${NAME} PROPERTIES CLEAN_DIRECT_OUTPUT 1 ) ADD_LIBRARY(${NAME}_static STATIC ${ARGN}) SET_TARGET_PROPERTIES(${NAME}_static PROPERTIES OUTPUT_NAME ${NAME} CLEAN_DIRECT_OUTPUT 1 ) INSTALL(TARGETS ${NAME} ${NAME}_static ARCHIVE DESTINATION lib LIBRARY DESTINATION lib ) ENDMACRO(CREATE_LIBRARY) INCLUDE_DIRECTORIES(include) CREATE_LIBRARY(file_sort src/file_sort.cpp) INSTALL(DIRECTORY include DESTINATION . PATTERN "*.h" )
Jako klasicky začínáme definicí projektu – PROJECT()
. Ale teď
tu máme novou věc: konstrukt MACRO() … ENDMACRO()
,
který nám nahraje příkazy do makra, které pak můžeme použít. Prvním
argumentem je název makra, dále následují pojmenování
argumentů samotného makra. Zde máme pouze jeden pojmenovaný argument –
NAME
.
Naše makro nazvané CREATE_LIBRARY
sestává z pěti volání.
ADD_LIBRARY()
nám vytváří sestavovací cíl pro knihovnu, kde prvním argumentem je název
cíle, poté následuje nepovinný argument typu knihovny (SHARED
–
sdílená, STATIC
– statická) a zbytek jsou názvy souborů se
zdrojovými kódy. Jelikož jsme v makru, používáme jako jméno proměnnou
${NAME}
vytvořenou při volání makra. Místo vypsání zdrojových
kódů tu máme ${ARGN}
, což je zbytek, nepojmenovaných, argumentů
při volání makra – u nás to tedy bude seznam zdrojových kódů. Nejdříve
vytvoříme sdílenou knihovnu, poté statickou. SET_TARGET_PROPERTIES()
nám tu nastaví, aby obě knihovny měly stejné jméno. A poslední volání,
INSTALL()
, řekne CMake, že zkompilované knihovny se mají
nainstalovat do adresáře lib
, opět relativně od
CMAKE_INSTALL_PREFIX
– možnost s ARCHIVE
nastavuje
adresář pro statické knihovny, LIBRARY
pro sdílené.
Po vytvoření našeho makra CREATE_LIBRARY()
ještě nastavíme, aby
se adresář include
přidal do INCLUDE_PATH
kompilátoru
– volání INCLUDE_DIRECTORIES()
. A už zavoláme samotné makro.
Jak jsem říkal, INSTALL()
jako první přijímá to, co chceme
instalovat. Tentokrát je to složka include
a všechny hlavičkové
soubory v ní (*.h
).
Teď se přesuňme k testování:
ENABLE_TESTING() ADD_EXECUTABLE(quicksort_test test/quicksort.cpp) ADD_TEST(quicksort_test quicksort_test) ADD_EXECUTABLE(basic_test test/basic.cpp) TARGET_LINK_LIBRARIES(basic_test file_sort_static) ADD_TEST(basic_test basic_test) ADD_CUSTOM_COMMAND(TARGET basic_test POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/test/abcd.txt abcd.txt DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/test/abcd.txt )
Aby se vůbec nějaké sestavovací cíle pro testování vytvořily, musíme
nejdříve zavolat ENABLE_TESTING()
.
Poté vytvoříme testovací binárky (pomocí ADD_EXECUTABLE()
). Test
fungování Quicksortu nepracuje
s naší knihovnou jako takovou (pouze inkluduje hlavičkový soubor
quicksort.h
a ten byl do INCLUDE_PATH
přidán již
dříve), avšak druhý test ano, takže ho musíme s čerstvě zkompilovanou
knihovnou slinkovat – a to s TARGET_LINK_LIBRARIES()
.
Prvním argumentem je sestavovací cíl, dalšími jsou knihovny – u nás jen
jedna.
ADD_TEST()
přidá test. Bere si název testu, název binárky, popř. argumenty příkazové
řádky – tady nepoužito.
Posledím voláním je ADD_CUSTOM_COMMAND()
,
které se v tomto případě napojí na cíl basic_test
a zkopíruje
soubor potřebný pro tento test.
Podmíněný překlad
Podmíněný překlad je další věc, která je stěžejní pro multiplatformnost aplikací. Pokud např. bude mít naše aplikace GUI a my budeme chtít, aby fungovalo jak pod Windows, tak pod Mac OS X a na strojích s X Serverem, bude potřeba, aby se pro každou platformu zkompilovalo vlastní (pokud nepoužijeme rovnou nějaký multiplatformní toolkit). Řekněme, že budeme mít následující strukturu souborů:
src/ main.cpp ... win32_gui/ main_window.cpp ... x11_gui/ main_window.cpp ... osx_gui/ main_window.cpp ...
Pak můžeme zkompilovat náš program nějak takto:
IF(UNIX) IF(APPLE) SET(GUI "osx") ELSE(APPLE) SET(GUI "x11") ENDIF(APPLE) ELSE(UNIX) IF(WIN32) SET(GUI "win32") ELSE(WIN32) MESSAGE(FATAL_ERROR "Unknown GUI type.") ENDIF(WIN32) ENDIF(UNIX) ADD_LIBRARY(gui STATIC ${GUI}_gui/main_window.cpp ...) ADD_EXECUTABLE(foo src/main.cpp ...) TARGET_LINK_LIBRARIES(foo gui)
Podmínkový konstrukt IF()
je
snad každému, kdo programuje, známý. Proměnné jako UNIX
,
APPLE
a WIN32
jsou definovány podle toho, na jaké
platformě zrovna CMake běží. Volání MESSAGE()
dokáže tisknout na obrazovku námi definované zprávy, prvním argumentem je typ
zprávy – myslím, že FATAL_ERROR
je všeříkající.
Dalším druhem podmíněného překladu je možnost, kdy naše aplikace dokáže
bez nějaké knihovny běžet, ale přítomnost oné knihovny může přidat nějakou
funkcionalitu. Pokud znáte GNU Autotools, tak tam je to povětšinou
dělané tak, že existuje kostra souboru config.h
(soubor
config.h.in
), do které jsou při generování vypsány hodnoty
různých maker. CMake toto umí taky:
OPTION(WITH_FOO "Include foo support." OFF) CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/config.h.cmake ${CMAKE_BINARY_DIR}/config.h) INCLUDE_DIRECTORIES(${CMAKE_BINARY_DIR})
OPTION()
dává uživateli možnost si v klikátku na Windows či v ccmake
vybrat, jestli chce tu kterou možnost povolit nebo ne. CONFIGURE_FILE()
přijímá jako argumenty plnou cestu ke kostře konfiguračního hlavičkového
souboru (v našem případě je to config.h.cmake
v adresáři se
zdrojovými kódy) a plnou cestu k výstupnímu souboru (config.h
v
sestavovacím adresáři). Takhle vypadá config.h.cmake
:
#ifndef CONFIG_H #define CONFIG_H #cmakdefine WITH_FOO #endif /* config.h */
CMake nahradí #cmakedefine WITH_FOO
za #define
WITH_FOO
, pokud je zapnuto WITH_FOO
, nebo za /*#undef
WITH_FOO*/
, pokud zapnuto není.
Pokud jde o hlavičkové soubory a knihovny, existují různé
CHECK_*
volání, viz wiki.
A posledním problémem je, že na různých systémech mohou být knihovny/hlavičkové soubory umístěny na různých místech. CMake má i pro toto
volání: FIND_LIBRARY()
,
FIND_PATH()
apod., více v dokumentaci.
Kompilace a podadresáře
Někdy je dobré, když je zdrojový kód ještě nadále členěn na podadresáře. I to se v CMake dá zařídit. Uvažujme o adresářové struktuře:
libfoo/ foo1.cpp foo2.cpp libbar/ bar1.cpp bar2.cpp src/ main.cpp ...
V rootu zdrojových kódů bude CMakeLists.txt
:
PROJECT(xyz) SET(DIRS libfoo libbar src) SUBDIRS(${DIRS}) INCLUDE_DIRECTORIES(${DIRS})
V libfoo
:
ADD_LIBRARY(foo foo1.cpp foo2.cpp)
Obdobně v libbar
. V src
pak:
ADD_EXECUTABLE(xyz main.cpp ...) TARGET_LINK_EXECUTABLE(xyz foo bar)
Závěr
CMake není jen další způsob, jak generovat Makefiles
,
jak by se mohlo na první pohled zdát. Je to velice dobrý způsob, jak generovat
mimo jiné Makefiles
– to „mimo jiné“ je asi jeho největší
výhoda. Problémy jsou, že musí být přítomen na stroji, kde chceme kompilovat,
a jeho trochu výřečnější syntaxe CMakeLists.txt
, co se týče
podmínek apod.