W ten weekend w końcu stanąłem do potyczki z konfiguracją Travis-CI. Długo to odkładałem, bo wiedziałem, że będą problemy. Chcę skonfigurować build system w dosyć niestandardowy sposób i nie mogę za bardzo skorzystać z wbudowanych w Travisa ułatwień. Zamiast tego muszę posiłkować się bashowymi skryptami. W wykonaniu zadania pomagają mi skrypty Freddie Chopina (link1, link2), któremu udało się skonfigurować podobny build system do tego, który chciał bym uzyskać. Nie udało mi się jeszcze wykonać poprawnego builda, dlatego dzisiaj opiszę ogólne spostrzeżenia do jakich udało mi się dojść. Nie składam jeszcze broni i pewnie pojawią się jeszcze kolejne wpisy w tym temacie opisujące mam nadzieję rozwiązanie problemu.

Założenia

Build system powinien:

  • Kompilować wszystkie targety unit testów przy użyciu standardowego gcc.
  • Kompilować wszystkie targety na docelową platformę przy użyciu gcc dla STM32.
  • Uruchamiać wszystkie unit testy i wykrywać, kiedy failują.

Najbardziej problematyczną kwestią jest tu punkt drugi, czyli kompilacja na STM32. Do tego zadania chciałem użyć tego samego kompilatora, którego używam lokalnie, czyli bleeding-edge-toolchain od Freddiego Chopina. Od jakiegoś czasu przy release’ach kompilatora binarka na Linuxa nie jest dostępna ze względu na problemy z kompatybilnością między różnymi wersjami systemu. Zamiast tego można ściągnąć skrypt umożliwiający kompilację ze źródeł. W teorii powinien odpalić się skrypt i zrobić wszystko za nas. Jednak w praktyce nigdy nie jest tak różowo. Skrypt zawsze się wywali i trzeba czytać komunikaty błędów, żeby naprawiać kolejne problemy.

Konfiguracja środowiska builda

Projekt na githubie powinien zawierać w głównym folderze plik .travis.yml będący ustawieniami konfiguracyjnymi dla Travis-CI. Zawarte w nim opcje konfiguracyjne definiują środowisko, w którym wykonywany jest build, oraz zawierają kroki wymagane do wykonania builda. Najpierw omówię konfigurację środowiska.

Pierwszą rzeczą, jaką powinniśmy określić  w pliku konfiguracyjnym jest rodzaj środowiska, w jakim będziemy budować nasz projekt. Do wyboru mamy dwie opcje:

sudo: false

Po umieszczeniu powyższej linii w pliku konfiguracjnym, nasz build jest uruchomiony w kontenerze. Charakteryzuje się on szybkim czasem uruchomienia (do 6s), do 4 GB dostępnego RAMu i brakiem możliwości używania komendy sudo.

sudo: required

Ta linia mówi Travisowi, żeby nasz build uruchomić w zwykłej maszynie wirtualnej. Jej czas uruchamiania jest dłuższy (do 50s), zawiera więcej RAM – 7.5GB i pozwala na używanie sudo.

addons:
  apt:
    packages:
    - texinfo
    - texlive

Powyższy kod pozwala na zainstalowanie dodatkowych elementów za pomocą apt-get. Przy czym nie wszystko da się w ten sposób zainstalować – mi się na przykład nie udało zainstalować gcc-4.8. Znalazłem za to obejście:

dist: trusty

Jest to wybór dystrybucji naszej maszyny wirtualnej. Do wyboru mamy dwie opcje: precise (domyślna) i trusty. Wersja precise ma zainstalowane tylko te narzędzia, które są potrzebne dla używanego języka programowania. Wersja trusty ma zainstalowane narzędzia dla większości języków, w tym interesujące mnie gcc-4.8. Tutaj ważna uwaga – jeżeli odpalamy wersję trusty na kontenerze (sudo: false), mamy dostępne tylko 512MB RAM. Poza tym ta opcja jest dopiero w wersji beta.

Update: Precise i Trusty to nazwy dystrybucji Ubuntu. Precise to Ubuntu 12.04, a Trusty to Ubuntu 14.04. To nie jest tak, że wersja Precise zawiera tylko narzędzia dla konkretnego języka, po prostu wersja Trusty jest nowsza i dlatego ma więcej zainstalowanych narzędzi i w nowszych wersjach.

Kroki wykonywane przy buildzie

Build wykonywany jest w następującej kolejności:

  • Najpierw wykonywany jest krok install, podczas którego instalowane są wszystkie zależności.
  • Dalej następuje krok script, który wykonuje właściwy build.
  • Później możliwy jest jeszcze krok deploy, służący do wgrania programu na docelowy system.

Istnieją jeszcze dodatkowe fazy takie jak np. before_install, before_script, after_success, czy after_failure. Dokładny opis kroków wykonywanych podczas builda jest opisany w dokumentacji Travisa.

Build może zakończyć się jednym z trzech stanów:

  • success
  • failed
  • errored

Istnieje subtelna różnica między failed i errored. Errored występuje jeżeli krok before_install, install lub before_script zakończy się niepowodzeniem. Build jest wtedy natychmiast przerywany. Failed występuje, jeżeli niepowodzeniem zakończy się krok script. Jeżeli składa się on z kilku części, błąd nie powoduje natychmiastowego przerwania. Kolejne części się i tak wykonają, a dopiero potem build zostanie oznaczony jako failed.

Ważnym następstwem występowania oddzielnych stanów failed i errored jest fakt, że krok after_failure wykona się tylko przy stanie failed. Było to dla mnie ważne, kiedy chciałem printować logi błędów, w przypadku błędu kompilacji toolchaina. Aby printować logi w after_failure, kompilację toolchaina musiałem umieścić w kroku script mimo, że logicznie bardziej by pasował do install.

Ograniczenia

Czasem build się nie udaje nie ze względu na jakiś błąd podczas wykonywania skryptów buildujących, tylko ze względu na wewnętrzne ograniczenia Travisa. Te ograniczenia to:

  1. Pojedyncza komenda trwa dłużej niż 50 minut na travis-ci.org.
  2. Nic nie zostało wyprintowane na konsolę przez 10 minut.
  3. Przekroczono dostępną pamięć RAM.
  4. Wielkość outputu konsoli przekroczyła 4MB.

Aby obejść punkt 2 można zrobić task działający równolegle z długo wykonującą się komendą, który printuje co jakiś czas znak na konsolę. W bashu może wyglądać to tak (kod wzięty z: link):

	{ time='0'; while true; do sleep 60; time="$((${time} + 1))"; echo "${time} minute(s)..."; done } &
	keepAlivePid="${!}"
	{dlugo wykonujaca sie komenda}
	kill "${keepAlivePid}"
	wait "${keepAlivePid}" || true

Podobną funkcjonalność ma komenda travis_wait.

Aby obejść punkt 3 możliwe jest wykorzystanie mechanizmu swap:

# Check free RAM
free

# Fix to overcome out of memory problems
SWAP=/tmp/swap
dd if=/dev/zero of=$SWAP bs=1M count=500
mkswap $SWAP
sudo swapon $SWAP
free

Aby obejść punkt 4 należy przekierować output konsoli do pliku i zastosować mechanizm zapobiegający punktowi 2. Jednak szczególnie w przypadku błędu wgląd w output konsoli się przydaje. Wtedy należy wyprintować ostatnie zapisane dane z konsoli przed wystąpieniem błędu w kroku after_failure. Można to osiągnąć dodając do pliku .travis.yml linie:

after_failure:
  - tail -n {liczba} {log}

W miejsce {liczba} należy podstawić ilość ostatnich linii z loga, jakie chcemy wyprintować, a w miejsce {log} należy podstawić ścieżkę do pliku z logiem konsoli.

Na punkcie 1 aktualnie się wykładam. Skrypt kompilacji toolchaina ze źródeł trwa dłużej niż 50 minut i na razie nie wiem co z tym zrobić.

Zwijanie logów konsoli

Mechanizm logów w Travisie potrafi zwijać logi w grupy. Jest to wykorzystywane np. do oddzielania poszczególnych kroków. Ten sam mechanizm można wykorzystać do zwijania własnych kategorii np. do oddzielnych testów, albo do oddzielnych elementów skryptu. Aby zwinąć logi należy wyprintować na konsolę następujące linie:

echo "travis_fold:start:nazwa"
echo "Tekst wyswietlany w logach Travisa"
echo "travis_fold:end:nazwa"

Podsumowanie

W ciągu weekendu puściłem ponad 30 buildów za każdym razem próbując jakiś małych zmian. Moje postępy można obserwować na stronie projektu na Travis-CI. Na razie cały czas stoję na kompilacji ze źródeł bleeding-edge-toolchain. Jak w końcu uda mi się wykonać poprawny build, opiszę dokładniej moją konfigurację.