× Aktuálně z oboru

SHIELD Experience Upgrade 7 – méně hledání a více zábavy [ clanek/2018052902-shield-experience-upgrade-7-mene-hledani-a-vice-zabavy/ ]
Celá zprávička [ clanek/2018052902-shield-experience-upgrade-7-mene-hledani-a-vice-zabavy/ ]

CMake – tutoriál

[ http://programujte.com/profil/10927-jakub-kulhan/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/14523-martin-simecek/ ]Google [ ?rel=author ]       7. 4. 2009       37 778×

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

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 vectoru řá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í Quicksort [ http://en.wikipedia.org/wiki/Quicksort ]u 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.

Odkazy

  1. Domovská stránka [ http://cmake.org/ ]
  2. Dokumentace verze 2.6 [ http://cmake.org/cmake/help/cmake2.6docs.html ]

Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2009032800-cmake-tutorial/ ].