O systemie budowania aplikacji bakefile, generującym odpowiednie pliki dla make i projekty dla różnych środowisk IDE, pisałem już w przeszłości. Jest to wspaniale narzędzie, szczególnie przy tworzeniu multi platformowego oprogramowania.
Dosyć często zdarza się, że chcemy dorzucić do makefile jakieś niestandardowe reguły oparte na wykonaniu kilku poleceń w powłoce systemowej. Bakefile zawiera odpowiednie mechanizmy do takich celów – reguła action, która pozwala wykonać dowolną komendę lub polecenie, moduły do kopiowania plików, katalogów, drzew katalogów - moduł datafiles (dostarczany w standardzie z bakefile), tworzenie plików wynikowych lokalizacji (mo) opartych na gettext, i wiele innych.
Niestety nie ma nic przyjaznego i wygodnego do wykonania kilku poleceń shellowych. Można wykorzystać regułę action, ale ta niestety posiada pewne ograniczenia. Pokrótce, chcemy zbudować plik zasobów (wx'owych) - resources.xrs
, który de facto jest zwykłym archiwum zawierającym m.in. pliki xrc. Najprościej zdefiniować następującą regułę:
<action id="xrs"> <dependency-of>all</dependency-of> <command> cd ../resources zip -9 resources.xrs *.xrc *.png cd ../build </command> </action> |
Co w wynikowym pliku makefile zostanie przedstawione w taki sposób:
xrs: cd ../resources zip -9 resources.xrs *.xrc *.png cd ../build |
O ile wersja makefile dla windowsowego VS będzie działać bez problemowo, to wersja pod uniksowe narzędzia rodziny GNU już niezupełnie. Jest to związane z budową programu make.
W wersji gnu, każde polecenie umieszczone w definicji wykonywane jest w oddzielnej kopii shella, co skutkuje nieprzewidywanym działaniem, różnym od zamierzonego. W Windowsie problem nie istnieje bo wszystko "leci" w jednej konsoli, w której odpalono nmake
.
Aby program make
wykonał polecenia w tej samej kopii shella, powinny one być zawarte w jednej linii, oddzielone od siebie średnikiem. Możliwe jest rozdzielnie wiersza na kilka linii wykorzystując znak kontynuacji \
. Niestety dodanie tak spreparowanego wiersza do pliku bakefile nie rozwiąże zbytnio problemu, bo wtedy dla odmiany, pod windoswem wystąpią problemy. Umieszczanie dwóch wersji z warunkiem if
również jest chybionym pomysłem.
Jest jedno proste i skuteczne rozwiązanie - wykorzystanie operatora &&
, który we wszystkich shellach działa identycznie. Można, więc bezproblemowo wykorzystać ten operator do połączenia kolejnych poleceń:
<action id="xrs"> <dependency-of>all</dependency-of> <command> cd ../resources && zip -9 resources.xrs *.xrc *.png && cd ../build </command> </action> |
Rozwiązanie to wykorzystywałem do tej pory w kilku projektach. Jest ono idealne dla kilu prostych poleceń. Niestety, gdy niezbędne jest wywołanie dużo większej ich liczby lub przekazanie skomplikowanych parametrów, staje się ono niezbyt wygodne i nieczytelne.
W takim wypadku najlepiej byłoby podawać kolejne polecenia i komendy w osobnych liniach, a sam bakefile zająłby się odpowiednią transformacją do odpowiedniego formatu akceptowanego przez daną powłokę.
Taki był mój cel, dlatego pokusiłem się o stworzenie prostej reguły shell, w której polecenia możemy podać w kilku wierszach. Wszystkie i tak zostaną wykonane w jednej i tej samej instancji shella.
<define-rule name="shell" extends="action"> <define-tag name="command"> <set var="__command"> <if cond="TOOLSET=='unix'"> $(value.replace('\n', '; \\\n')) </if> <if cond="TOOLSET=='win32'"> $(value) </if> </set> </define-tag> </define-rule> |
W istocie jest to prostsze rozszerzenie standardowej reguły action, gdzie zawartość tagu command
jest podawana odpowiednim transformacjom, w zależności od platformy przeznaczenia generowanego formatu wyjściowego. Dla uniksowych toolsów dodajemy na końcu każdej linii średnik i znak kontynuacji, a dla windowsowych przepisujemy bez zmian.
Bakefile napisane jest w pythonie, możemy w plikach bakefile wykorzystać proste wyrażenia i funkcje tego języka, dzięki czemu system jest bardzo elastyczny i można wiele przydatnych funkcji i możliwości stworzyć.
Droga do prostego wyrażenia
value.replace('\n', '; \\\n') |
była długa, w skrajnym wypadku było nawet
'; \\\n'.join([x.strip() for x in value.splitlines()]) |
ale dobrze się skończyło ;)
Teraz możemy łatwo i czytelnie stworzyć definicje dla make, oparte na wykonaniu kilku poleceń powłoki:
<shell id="xrs"> <dependency-of>all</dependency-of> <command> cd ../resources zip -9 $(OUTPUTDIR)/../resources.xrs *.xrc zip -9 $(OUTPUTDIR)/../resources.xrs images/*.png cd ../build </command> <clean-files>$(OUTPUTDIR)/resources.xrs</clean-files> </shell> |
Przy okazji, nie zapominajmy po sobie sprzątać! Co w naszym przypadku czyni tag clean-files
, który dodaje swoją zawartość do listy plików do usunięcia w czasie wykonania make clean
.
I tutaj mała uwaga. Nie wszystkie dostępne tagi opisane w rozdziale 4 dokumentacji bakefile są możliwe do wykorzystania w we wszystkich formatach wyjściowych bakefile, co dogłębnie można zobaczyć na plikach projektów, dla których większość nich jest niedostępna. Użycie takiego tagu kończy się wyrzuceniem w czasie przetwarzania bakefile błędu typu "unknown target tag...". Może to być uciążliwe, gdy korzystamy z hurtowego, wsadowego generowania przez bakefile_gen
kilku rodzaju wyjściowych formatów plików wyjściowych.
Dziwne jest to, że w definicjach formatów dla nieobsługiwanych tagów twórcy nie zdefiniowali chociażby "wyplucia" ostrzeżenia o zaistniałej sytuacji, jak to ma miejsce na przykład w przypadku reguły action lub tagu depends-on-file
dla plików projektu. Na pewno byłoby to dużo lepszym rozwiązaniem, niż przerwanie przetwarzania. Może należy im przypomnieć o tym problemie ;)