Začíname s ROS! Časť tretia – Catkin

catkinAk bola vaša doterajšia programátorska minulosť pevne zviazaná s pohodlnými IDEčkami, vykonávajúcimi všetku „špinavú“ kompilovaciu a linkovaciu robotu za vás, prvý CMakeList s ktorým ste sa v ROS stretli musel byť pre Vás určite nepríjemnou studenou sprchou. Áno v ROS to bez CMakeListov, linkovania a kompilovania z podstaty fungovania celého frameworku nepôjde ale podobne ako konzoly ani buildovacie systémy nie sú neprekonateľnou prekážkou. Aby sme začínajúcim vývojárom pomohli vôbec zorientovať sa, v treťom dieli nášho seriálu sa na buildovacie systémy pozrieme bližšie, ukážeme si aké možnosti vo všeobecnosti pri kompilovaní máme a pokúsime sa vysvetliť pozadie a príčiny vzniku catkinu.

Inšpiráciou pre tento článok bol pôvodný textA Gentle Introduction to Catkin“ od Johnatana Bohrena z februára 2014, čiže ak by ste mali nejaké nejasnosti alebo by vás zaujímala problematika buildovacích systémov hlbšie, odporúčame pôvodný text.

Úvod do buildovacich systémov

O tom že zdrojový kód programu je pred vykonávaním procesorom potrebné najprv preložiť do strojového kódu, o tom tu písať nebudeme, to všetci isto viete. Buildovanie by bolo bezproblémové asi iba ak by všetky počítače boli úplne rovnaké, existovala by iba jedna verzia jedného operačného sýstému s jediným prekladačom a na každom počítači by bol OS  nainštalovaný v rovnakom priečinku. Tým že praxi je to oveľa oveľa divokejšie sa celý proces buildovania značne komplikuje a vyžaduje nasadenie rôznych nástrojov, ktoré si pri správnom nastavení parametrov dokážu s týmito odlišnosťami poradiť.

1. g++ build

Priame buildovanie cez príkaz g++, je najnižšou úrovňou prekladu, s trochou nadsádzky môžeme povedať že je to buildovací assambler. Používa sa ak píšete jednoduchšie programy, prípadne ak z nejakého dôvodu chcete mať plnú kontrolu nad každým detailom prekladu. Pre ilustráciu ako g++ vyzerá v praxi sa pokúsime zbuildovať základný tutoriálový ROS „hello_world“ node:

ros_tutorial

ros_build_2

Ak by hello_world_node nemal žiadne externé „dependencies“ vystačili by sme si s jednoduchým príkazom g++ hello__world_node -o hello_world_node. Ale tým, že náš node závisí od knižníc roscpp a rosconsole, ktoré je pri buildovaní nevyhnutné nalinkovať, sa nám množstvo parametrov v g++ príkaze zväčšuje a značne zneprehľadňuje samotný zápis. Kým máme iba dve externé knižnice dá sa v tom ešte orientovať ale ak je tých knižníc napríklad 10, pričom pre každú z nich treba správne nastaviť zdrojové cesty ku hlavičkovým a .so súborom, nehovoriac o kompilovacích, linkovacích flagoch a dalších drobnostiach, g++ prístup sa stáva v praxi nepoužiteľným.

2. GNU MakeFiles

Ako ste videli v predchádzajúcom príklade, g++ príkaz može veľmi rýchlo narásť do dľžky pri ktorej je problematické sa v príkazovom riadku orientovať a niečo meniť.  Riešenie ako si čiastočne prácu uľahčiť ponúka buildovací systém GNU.

Princíp GNU Makefileov je jednoduchý – všetky potrebné kompilovacie príkazy, premenné, flagy a cesty ku knižniciam sa vopred spíšu do textového súboru  (viď Makefile nižšie) a tento súbor následne buildovací systém GNU využije na vygenerovanie g++ príkazov potrebných pre samotný build.

cmake_01

hello_world Makefile

Preklad sa spúšta príkazom „make“ v rovnakom priečinku ako sa nachádza samotný Makefile, pričom GNU v našom prípade vygeneruje dva g++ príkazy:

gnu2

Na rozdiel od „čistého“ g++ prístupu, GNU v prvom príkaze prekladá samotný hello_world kód do .o súboru a v druhom príkaze ho linkuje so súvisiacimi knižnicami do výsledného spustiteľného súboru. Tento prístup umožňuje prekladať jednotlivé zdrojové súbory samostatne, bez potreby buildovať celý projekt, ak nastala zmena iba v jednom zdrojáku.  Ďalšou výhodou g++ je možnosť „includnuť“ konfiguračný .pc súbor package-u, na ktorom závisí naša aplikácia s tým, že v .pc súbore sú zadefinované všetky flagy a cesty ku potrebným súborom pre daný package, čiže ich nemusíme pracne vypisovať sami.

Buildovanie cez GNU je síce prehľadnejšie ako priamy g++ prístup ale vývojár musí stále poznať a definovať pravidlá a buildovacie flagy pre kompiláciu knižnic a spustiteľných súborov. Z týchto dôvodov sa GNU používa sa iba pri malých až stredne veľkých projektoch a pri buildovaní zložitejších projektov sa prechádza na pokročieljší CMake buildsystém.

3. CMake

CMake je opať o triedu vyšší level prekladu ale s predchádzajúcim spôsobom úzko súvisí. Na Linuxovsko/Unixovských platformách totiž CMake priamo generuje GNU Makefile-y namiesto vývojára. Tým nielen šetrí čas ale najmä eliminuje chyby, keďže GNU je veľmi citlivý na správnu syntax. CMake má svoj vlastný jazyk, pomocou ktorého pomerne jednoducho deklarujete na ktorých binárnych packageoch v systéme závisí váš kód a o zvyšok práce sa postará buildsystém. Čo robí CMake silným nástrojom je jeho univerzálnosť, keďže dokáže generovať nielen GNU Makefile-y ale aj Mac OS XCode Project file-y a Microsoft Visual Studio Project file-y.

Cmake pri buildovaní vyhľadáva súbor „CMakeLists.txt“, v ktorom sú špecifikované zdrojové súbory a „dependencies“ pre daný package. V praxi „CMakeLists.txt“ pre náš „hello_world_node“ vyzerá nasledovne:

cmake01

Najdôležitejším riadkom je find_package(…), ktorý v systéme daný package nielen vyhľadá ale zároveň zadefinuje všetky potrebné CMake premenné pre nalinkovanie externých knižníc daného package-u, nastaví cesty ku hlavičkovým súborom ${roscpp_INCLUDE_DIRS}, ku spustiteľným súborom ${roscpp_LIBRARY_DIRS} a ku závislostiam samotného roscpp ${roscpp_LIBRARIES}, čiže v tomto prípade ku package-u rosconsole. 

Ak máme CMakeLists hotový môžeme sa pustiť do prekladu. Je zaužívaným zvykom, že v priečinku nášho package-u si pred samotným prekladom vytvoríme nový priečinok „build“ do ktorého v prvom kroku príkazom „cmake .. “ vygenerujeme GNU Makefile a v druhom kroku už buildujeme podobne ako v predchádzajúcom prípade príkazom „make hello_world_node„. V konzole to vyzerá nasledovne:

cmake_real

Ak všetko úspešne prebehne v „build“ priečinku nám pribudne viacero súborov, vrátane vygenerovaného GNU MakeFile-u a samotného spsustiteľného súboru „hello_world_node:

cmake_tree

Usporiadanie všetkých súborov súvisiacich s prekladom do build priečinka je výhodou oproti GNU, keďže sa zachováva poriadok v organizácii súborov aj pri mnohonásobnom opakovanom prebuildovávaní.

Cmake je ideálny nástroj pre širokú škálu open-source aplikácií, keďže je s ním možné pomerne jednoduchým spôosobom prekladať aj náročnejšie projekty. Kým sú naše dependencies v systéme prítomné ako nainštalované „binary packages“, CMake so svojími find_package(…) a príbuznými nástrojmi je ideálnou voľbou.

4. Catkin

CMake sám o sebe by úplne postačoval aj pre potreby ROS ale iba za predpokladu ak by všetky package frameworku boli binárnymi package-mi. Ak už ale máte s ROS určité skúsenosti, sami dobre viete, že binárnymi package-mi sú iba základné komponenty ROS systému a väčšina softwaru dostupného na repozitároch sa po stiahnutí kompiluje.

Úlohou catkinu je pomocou špecifických CMake makier a funkcí dosiahnuť stav kedy sa práve package prekladané zo zdrojových kódov budú v systéme správať ako regulárne nainštalované binárne package. Inak povedané, aby vývojári ROS nemuseli vvýjať nový buildsystém prispôsobený potrebám open-source robotického softwaru, použili existujúci CMake aj keď nakonfigurovaný v nie celkom štandardnom režime.

V praxi sa na našom tutoriálovom „hello_world“ príklade zmena prejaví pridaním dvoch riadkov „find_package(catkin REQUIRED)“  a „catkin_package()“ do „CMakeLists.txt„:

cmake_catkin_01

Ak sa teraz pokúsime predchádzajúcim spôsobom zbuildnúť existujúci package, ďaleko sa nedostaneme lebo Cmake nám namiesto spustiteľného súboru vychrlí iba zopár errorov:

cmake_package

Ako vidíme v prvých riadkoch, pre úspešný build nám ešte niečo chýba. Nato aby „hello_world“ mohol vystupovať ako plnohodnotný catkin package, na ktorom môžu závisieť ostatné package musíme ešte zadefinovať jeho „package.xml“ manifest.

Tento XML súbor je akousi hlavičkou nášho package-u, obsahuje informácie o programátorovi, licencii ale najmä o závislostiach od iných package-ov. V manifeste „hello_world“ projektu nižšie si všimnite najmä buildtool, build a run depend riadky:

package_xml

Po zadefinovaní „package.xml“ v koreňovom adresári projektu už príkazy „cmake .. “ aj „make hello_world_node“ fungujú, avšak s jedným podstatným rozdielom oproti základnému CMake spôsobu – spustiteľný súbor „hello_world_node“ sa už nenachádza priamo v build priečinku ale je uložený na  build/devel/lib/hello_world_tutorial/hello_world_node , čo je nielen prvým prejavom prikazu catkin_package() v CMakeListe ale má aj kritický význam pre spôsob fungovnia celého catkinu.

Nezabúdajme, že hlavnou úlohou catkinu, je zabezpečiť aby sa package skompilované zo zdrojových kódov správali ako štandardne inštalované binárne package. Mechanizmus ktorým sa to docieli sa skrýva práve v pričinku devel. Ak sa pozriete na usporiadanie súborov v devel, vidíte štruktúru pripomínajúcu koreňový adresár UNIX-ovských systémov, čiže priečinky ako bin, lib, share, etc a ďalšie. Konkrétne pre náš prípad je štruktúra devel priečinka nasledovná:

devel

Všimnite si, že catkin automaticky vygeneroval .pc a .cmake konfiguračné GNU a CMake súbory, vďaka čomu je možné použit náš package v nadväzujúcich projektoch priamo ako „dependency“. Nato aby systém dokázal náš package rozpoznať, je ešte potrebné „nasourcovať“ súbor „setup.bash“. ROS konzolové príkazy ako rospack list, roscd a rosrun by už potom mali byť schopné náš package nájsť.

Už sa blížime ku najpoužívanejšiemu „catkin_make“ príkazu, akurát nám ešte ostáva spraviť jednu zásadnú zmenu a tou je „Out-of-Source“ build.

Doteraz sme pri CMake spôsobe prekladu vytvárali v koreňovom adresári nášho package-u priečinok build, do ktorého boli ukladané všetky súbory spojené s prekladom. Nie je na tom principiálne nič zlé ale v praxi sa v pri CMake aj catkine používa prehľadnejší „out-of-source“ build s nasledujúcou štruktúrou:

out-source

Týmto sa prehľadne oddelia zdrojové kódy od súborov vygenerovaných pri builde a výsledkov prekladu v devel priečinku. Build prebieha takmer rovnako ako v predchazajúcom prípade, akurát v cmake príkaze ešte potrebujeme zadefinovať cestu ku žiadanému „CMakeLists.txt“ a tiež ku priečinku, do ktorého chceme uložiť výsledné súbory.  V konzole sa to zapíše nasledovne:

build2

Predstavme si teraz situáciu, že do priečinka src pridáme ku „hello_world“ ešte jeden package napríklad „turtlesim_node“ a obidva chceme zbuildovať naraz jedným príkazom. Základný príkaz cmake.. v tomto prípade stačiť nebude. A aby sa nám to podarilo spraviť, potrebujeme zjednocujúci „koreňový catkin CMakeLists.txt„. Až ten nám umožní buildovať viacero package-ov naraz, pričom do existujúcej štruktúry ho je možné pridať buď ručne alebo pomocou príkazu „catkin_init_workspace„. Štruktúra súborov bude vyzerať nasledovne:

catkin_make

Stále platí, že jednoltivé package môžeme buildovať ručne cez „cmake ..“ ale prečo by sme to robili ak sme sa konečne dostali k najpoužívanejšiemu príkazu v ROS – ku „catkin_make„. Vďaka nemu vieme totiž buildovať naraz celý „workspace“, čiže nielen naše package ale aj package stiahnuté s externých repozitárov.

Ak sa teraz na konci tohto článku spätne zamyslíte nad celým procesom, ktorý pri builde prebieha, čiže :

soucre -> [ catkin -> CMake -> GNU MakeFile -> g++ ] ->  devel

verím, že vysvetlovať čím je catkin pre vývoj softwaru v ROS užitočným už viac netreba..

František Ďurovský