Zarządzanie zależnościami to jedna z mniej przyjemnych części kodowania w C++.
Pisząc w Javie mamy Mavena (czy Gradle), Python ma pip, PHP – composer, Node – npm, etc., etc. Każde z tych narzędzi łączy prostota użycia. Dla PHP wystarczy wpisać composer install i wszystkie zależności zapisane w prostym pliku json są zaczytywane do projektu. W przypadku nowego projektu można dodawać zależności używając composer require <nazwa_paczki>. Repozytorium wszystkich paczek jest dostępne pod adresem https://packagist.org/, poszczególne paczki to po prostu podpięte repozytoria gitowe. Proste i przyjemne w użyciu. Pozostałe menadżery działają prawie identycznie.
Teraz jak to wygląda w przypadku C++:
Jeżeli używamy Linuksa może nas poratować systemowy manadżer pakietów. W przypadku Debiana wydajemy komendę apt-get install libsdl2-dev i automatycznie instalują się nam odpowiednie nagłówki i biblioteki wymagane przy użyciu SDL. OS X ma trochę gorzej, ale można się zaopatrzyć w Homebrew, który działa podobnie jak apt. Najgorzej ma Windows – przez wiele lat nie doczekał się menadżera z prawdziwego zdarzenia. Do dzisiaj instalując jakiś program musimy przedzierać się przez nie zawsze budzące zaufanie strony, przeklikiwać się przez instalator uważając przy okazji żeby nie zainstalować jakiegoś toolbara. Gdy mowa o bibliotekach sprawa wygląda nie lepiej – jeżeli twórca udostępnia skompilowaną bibliotekę pod nasz kompilator, dodatkowo z tymi ustawieniami jakich oczekujemy to super – wystarczy ją pobrać, wypakować i dodać do ścieżek przy kompilacji. Ale gdy dostępne są tylko źródła? Pobieramy, rozpakowujemy i przy odrobinie szczęścia znajdziemy tam projekt do Visuala który po odpaleniu i skompilowaniu wypluje naszą libkę. W 90% przypadków zastaniemy tam jedynie plik configure – na Windowsie na nic on się nie zda.
Teraz wystarczy sobie wyobrazić sytuacje gdy chcemy skompilować projekt używający wiele bibliotek, a każda z nich ma jeszcze swoje zależności i na dodatek używają swojego systemu do budowania. Koszmar.
Opiszę po krótce jakich rozwiązań można użyć i co wybrałem ja.
1. Shell/Batch – najprostsze rozwiązanie. Wykonuje serie komend potrzebną do wygenerowania projektu. Zdarzało mi się używać, ale ma raczej same wady – rekompilowany jest cały projekt a nie tylko te pliki które się zmieniły, ograniczenie do jednej platformy. W praktyce nie stosuje się.
2. Makefile – wystarcza do prostych projektów, gdzie wymagamy wygenerowanie binarki na podstawie kodu źródłowego. Dobrze napisana konfiguracja dba o to, żeby kompilowane były tylko zmodyfikowane pliki. Sam się kiedyś na tym złapałem, gdyż mój Makefile nie brał pod uwagę zmian w plikach nagłówkowych i przez długi czas szukałem rozwiązania pewnego buga – wystarczyło zrobić clean i zbudować program jeszcze raz. Większe projekty używają antycznego Autotools z dodatkiem dziwnego systemu makr M4, który generuje configure a ten z kolei Makefile. Obydwa pliki są nieedytowalne (przykładowy configure dma ponad 25k linii kodu w shellu). Dla Linuksowych narzędzi to zazwyczaj standardowy sposób kompilacji: ./configure && make && make install
3. Ninja – szybki system czasami zastępujący Make, służy jedynie do budowania projektu. Raczej nie pisze się bezpośrednio pliku konfiguracyjnego, a używa innych systemów do wygenerowania go (tak jak configure tworzy Makefile)
4. SCons – używa Pythona, nie widziałem wiele projektów które by go używały. Spotkałem się z opinią, że jest bardzo wolny.
5. CMake – nie zajmuje się budowaniem projektu a raczej tworzy on konfigurację dla danego systemu – na Windowsie wygeneruje .vcproj dla dowolnej wersji Visuala, na Linuksie dostaniemy Makefile czy Ninja. Używa go wiele projektów co wydawało się dobrym wyborem. Chciałem zacząć go używać, przeglądnąłem dokumentację, oglądnąłem prezentację na jego temat i odrzuciło mnie – nie chcę uczyć się następnego języka aby tylko zbudować projekt. Do konfiguracji używany jest niestandardowy język oparty na makrach. Niepotrafiłem go skonfigurować żeby budował projekt tak jak tego oczekiwałem, a bardziej skomplikowana konfiguracja wymaga masy kodu – przykład. Szukałem dalej.
5. GYP – ten zapowiadał się ciekawie. Został stworzony przez Google i jest używany w takich projektach jak Chromium. Napisany w Pythonie, działa na tej zasadzie co CMake, ale używa JSONa. Nie chciałem używać Pythona (kolejna zależność) i mam wrażenie, że JSON nie nadaje się do pisania takich plików. Ciekawa opcja ale natknąłem się na coś innego
6. Premake – używa Lua, którego składnia przypomina YAMLa połączonego z JSONem. Prosty plik konfiguracyjny to kilka linii w pliku premake5.lua, a użycie ogranicza się do wpisania premake5 vs2013 i otrzymujemy plik projektu który zadziała z Visualem (zasada działania jak GYP i CMake). Z racji tego, że jest oparty o Lua który jest pełnoprawnym językiem skryptowym można napisać bardziej skomplikowane akcje gdy zajdzie taka potrzeba. Dokumentacja na wiki projektu jest krótka, ale opisuje to co ważniejsze – warianty buildów, dodatkowe flagi i inne. Wydaje się najlepszym rozwiązaniem na teraz. Przykładowy plik konfiguracyjny.
Pozostaje jeszcze kwestia zależności. Na Linuksie możemy poprosić użytkownika aby przed zbudowaniem projektu wykonał serię komend apt-get, które dociągną wymagane zależności. Na Windowsie to odpada, a poza tym nie wszystkie zależności są dostępne w ten sposób. Jedynym sensownym rozwiązaniem jest ściągnięcie bibliotek w formie kodu i dołączenie ich do projektu. Z pomocą przychodzi Git ze swoim Submodules – w pliku .gitmodules definiuje się zależności i adresy do repozytoriów. W ten sposób po zklonowaniu projektu wystarczy wydać polecenie git submodule update –-init i wszystkie zależności zostaną dociągnięte.
Do tej pory używałem projektu wygenerowanego przez Visual Studio, a gdy potrzebowałem skompilować program na Linuksie (Travis CI) używałem prostego skryptu shellowego. Zamierzam przenieść się na Premake i dopiero zobaczę czy to dobre rozwiązanie.
Jestem ciekaw jakie macie podejście do problemu poruszonego w tym wpisie – zapraszam do komentowania.