Gradle build

Słowem wstępu

Gradle to build tool który wyrósł na kilku sprawdzonych technologiach:
  • Ant - z którego korzystamy w tej chwili - to narzędzie świetnie nadaje się do prostych operacji na systemie plików, paczkowania, etc
  • Maven - narzędzie wyrosłe na Ancie, wprowadziło koncepcję projektu, jako czegoś więcej niż tylko pliki w katalogach - teraz na projekt składa się kod, zależności (czyli biblioteki zewnętrzne) oraz zadania (już nie targety w stylu make'a, jak w Ancie, a zadania - czyli bardziej skomplikowane pomysły). Powyższe zdanie to rzecz jasna uproszczenie. Dodatkowo Maven wprowadził strukturę plików w projekcie która doskonale pasuje do Test Driven Development (wyraźny podział na produkt i testy, każda z części składa się z kodu i zasobów - czyli konfiguracji, danych testowych, etc - wszystkeigo czego nie kompilujemy). Chyba największy plus Mavena - repozytoria bibliotek, np http://mvnrepository.com/ . Są to serwery, które przechowywują i udostępniają zewnętrzne biblioteki w różnych wersjach i pozwalają na ich pobranie, przez co we własnym repozytorium nie trzeba trzymać cudzego kodu (i zawsze ma się pewność, że biblioteki są w tej wersji której potrzebujemy)
  • Groovy - język programowania wyrosły z Javy, kompilowany do tego samego bytecode'u (więc można korzystać z klas w nim napisanych w Javie, Scali, etc i na odwrót). Jest to IMO najwygodniejszy język dynamiczny na JVM (dynamiczny znaczy, że klasy mogą się zmieniać podczas wykonania programu, np możemy dodawać metody, kompilować klasy, etc). Pozwala na bardzo wygodne tworzenie DSLi (Domain Specific Language), które są tłumaczone do "żywego" kodu Groovy.

Gradle ma wbudowany ant builder, który pozwala opisać wszystko co da się opisać w Ancie przy pomocy Gradle, oraz importować Antowe pliki XML do buildów Gradle. Z Mavena czerpie strukturę katalogów oraz system rozwiązywania zależności (komunikacji z repozytoriami bibliotek; udostępnia też system Ivy do tego samego, ale nigdy go nie używałem). Same buildy to de facto skrypty Groovy, ale to nie znaczy, że trzeba znać Groovy żeby z tego korzystać. W zamian dostajemy pliki build w których możemy używać konstrukcji programistycznych w stylu for, while, kolekcji w stylu list i map, etc - build przestaje być sztywny, a zaczyna faktycznie przypominać zestaw poleceń.

Użycie

Przygotowanie repozytorium

Po sklonowaniu repo nie trzeba robić NIC. Gradle automatycznie (przy pierwszym uruchomieniu) potworzy katalogi, których brakuje, dociągnie biblioteki, etc, etc.
Przypominam, że w tym momencie Gradle jest używane tylko na niektórych gałęziach.

user@host:X$ git clone git@nlp.pwr.wroc.pl:liner2
user@host:X$ cd liner2
user@host:X/liner2$ git checkout liner25_dev_gradle

Wybór gradle

JEŚLI CZYTASZ TO PO RAZ PIERWSZY, TO ZWRÓĆ UWAGĘ NA TEN AKAPIT!

Do repozytorium dołączyłem wrapper dla gradle - to mały skrypt, który automatycznie dociąga samo gradle, jeśli go brakuje. Jest stworzony tak, żeby działać identycznie jak lokalna dystrybucja.
Jeśli ktoś ma zainstalowane gradle lokalnie (co osobiście polecam, choć nie jest to wymagane), to niech używa lokalnego programu (zapewne będzie nazywać się "gradle", po prostu). Jeśli nie mamy lokalnej dystrybucji, to używamy wrappera: skryptu "gradlew" na unixach i podobnych (na Macach też), "gradlew.bat" na windowsie.

W dalszej części dokumentu przykłady będę podawał tak, jakbyśmy mieli lokalną instalację, ale poniżej przedstawiony jest przykład jak ta sama linia będzie wyglądać u różnych osób:

user@host:X/liner2$ gradle build            # z lokalną instalacją pod nazwą "gradle" 
user@host:X/liner2$ ./gradlew build         # z użyciem wrappera na linuksie i macu
C:\X\liner2> ./gradlew.bat build            # z użyciem wrappera na windowsie

Kompilacja

Aby skompilować (i przetestować, patrz niżej) wszystkie moduły wykonujemy:

user@host:X/liner2$ gradle build

Aby skompilować pojedynczy moduł wykonujemy

user@host:X/liner2$ gradle <nazwa modulu>:build

na przykład

user@host:X/liner2$ gradle g419-corpus:build

Kompilacja, a budowanie

Wykonując taska "build" de facto każemy Gradle nie nie tylko skompilować kod, ale też wykonać testy, wyprodukować paczki JAR, etc.

Sama kompilacja to task "compile". Osobiście nie polecam go używać, ponieważ nie produkuje on paczek JAR, nie wykonuje testów - ogółem jedyne co się dzieje to przepuszczenie plików *.java przez kompilator i uzyskanie plików *.class.

Gdzie co znaleźć?

Większość tasków wymaga, aby w katalogu w którym leży moduł na którym wykonujemy taska był katalog build.
Katalog ten jest tworzony automatycznie. Jako, że każdy projekt jest też modułem, to w głównym katalogu projektu znajdziemy taki folder, ale będzie on prawie pusty - nic co tam się znajduje nas nie interesuje.
Ciekawsze są dla nas katalogi <nazwa modułu>/build, np <repo root>/g419-corpus/build. Większość jego zawartości nie jest zbyt ciekawa - są to pliki tymczasowe, cache skompilowanych klas, etc. Najważniejszy jest folder libs w którym znajdziemy paczki JAR.

Kompilacja CRFPP i zależności między modułami

CRFPP jest kompilowane automatycznie - do tego służy moduł g419-external-dependencies. Nie musimy nic robić, a CRFPP zostanie skompilowane (lub zrekompilowane, jeśli jest taka potrzeba).
Jeśli katalog <repo root>/g419-external-dependencies/crfppWorkspace nie istnieje, to gradle kompiluje CRFPP ponownie, jeśli zaś istnieje - używa jego zawartości, aby dołączyć CRFPP wraz z bindingami do projektu.

W skrócie - jeśli chcemy zrekompilować CRFPP usuwamy w.w. katalog crfppWorkspace, w przeciwnym wypadku nie dotykamy modułu external-dependencies. Najlepiej jest to zrobić za pomocą taska cleanCRFPP:

user@host:X/liner2$ gradle g419-external-dependencies:cleanCRFPP

Więcej szczegółów na ten temat w rozdziale "Czyszczenie projektu".

Jeśli któryś moduł zależy od innego (np g419-liner2-cli zależy od g419-liner2-api), to przed zbudowaniem modułu zależnego (np cli) Gradle wyprodukuje paczki JAR dla zależności (np modułu api). Nie zawsze będzie to pełen build - być może będzie to tylko paczkowanie, bez wykonywania testów.

Uruchamianie programu

Jeśli chcemy uruchomić klasę z mainem danego modułu w sposób który przekłada się na coś takiego:

user@host:X/liner2$ java -jar <nazwa modułu>/build/libs/<nazwa JAR> -cp <CLASSPATH> <ARGUMENTY>

czyli na przykład:

user@host:X/liner2$ java -Xmx4024M -cp (...) -jar ./g419-liner2-cli/build/libs/g419-liner2-cli.jar pipe -f 00102215.xml -m cfg.ini -i ccl -v

wykonujemy

user@host:X/liner2$ gradle <nazwa modułu>:run -PappArgs=<ARGUMENTY>

czyli np:

user@host:X/liner2$ gradle g419-liner2-cli:run -PappArgs="pipe -f 00102215.xml -m cfg.ini -i ccl -v" 

Należy zwrócić uwagę na cudzysłów wokół argumentów - shell musi przekazać wszystkie argumenty do Gradle jako pojedynczy string.

Gradle automatycznie zbuduje moduł który próbujemy odpalić. Oznacza to kompilację modułów od których odpalany zależy. Jak było wspominane, dla zależności niekoniecznie jest przeprowadzany pełen build, a czasem jedynie paczkowanie.

Czyszczenie projektu

Czasem zdarza się tak, że javac zaczyna wariować, sam interpreter Javy też nie pomaga, błędy się sypią, a nikt nie wie czemu. W takich chwilach warto usunąć wszystko co było dotychczas skompilowane i skompilować wszystko od nowa.

Aby usunąć WSZYSTKO co było skompilowane, wykonujemy:

user@host:X/liner2$ gradle clean

Aby oczyścić tylko pojedynczy moduł:

user@host:X/liner2$ gradle <nazwa modułu>:clean

na przykład:

user@host:X/liner2$ gradle g419-liner2-cli:clean

Czyszczenie zewnętrznych zależności i CRFPP

Moduł external-dependencies dostarcza zależności, których nie znajdziemy na repozytorium mavena, tzn princetonAdapter i CRFPP.

Task clean dla tego modułu spowoduje usunięcie wszystkich danych tymczasowych - zarówno tych dla adaptera, jak i CRFPP. Task cleanCRFPP spowoduje oczyszczenie jedynie CRFPP, ale podczas paczkowania CRFPP może nie zostać przekompilowane (ponieważ cała paczka wynikowa będzie już zbudowana, więc nie będzie takiej potrzeby).

Ostatecznie - jeśli nie masz 100% pewności, że wiesz co robisz i że dokładnie to chcesz zrobić - używaj taska clean (dla całego projektu, lub dla modułu), zamiast cleanCRFPP.

Zależności

W wielkim skrócie, aby dodać nową bibliotekę do modułu:
  1. wchodzimy na stronę http://mvnrepository.com/
  2. wyszukujemy bibliotekę której potrzebujemy, np "commons-io"
  3. wybieramy wersję, która nas interesuje
  4. otwieramy zakładkę "Gradle" (pod tabelką, której pierwszy wiersz to "Artifact")
  5. kopiujemy tekst tam przedstawiony, np 'commons-io:commons-io:2.4'
  6. otwieramy plik build.gradle
  7. do sekcji dependencies dodajemy linię compile <zawartość schowka>, np compile 'commons-io:commons-io:2.4'
  8. profit

Słowo compile z punktu 7 oznacza, że ta biblioteka ma być dostępna podczas kompilacji danego modułu. Możemy również kazać Gradle dociągać tą bibliotekę do kompilacji testów (testCompile), do uruchomienia programu (runtime) lub uruchomienia testów (testRuntime). Wszystko, co jest dostępne podczas kompilacji będzie też dostępne podczas uruchomienia. Wszystko, co jest dostępne dla właściwego kodu jest też dostępne dla testów.

Jeśli chcesz po prostu mieć dostęp do libki z kodu, użyj słowa compile.

Tworzenie zależności na inne moduły, na kod którego nie znajdziemy na żadnym repozytorium lub na kod który znajduje się na innym repozytorium niż powyższe jest wykonalne, ale na razie tego nie opisuję - w razie czego dostarczę linki, lub rozpiszę się tutaj.

UWAGA! Jeśli Twoje IDE nie widzi zależności, to przeczytaj kolejny rozdział!

Integracja z IDE

Domyślnie można wygenerować pliki projektu dla 2 IDE: Eclipse i IntelliJ IDEA. Aby to zrobić wykonujemy (dla Eclipse):

user@host:X/liner2$ gradle eclipse

lub (dla IntelliJ):

user@host:X/liner2$ gradle idea

Nie mam doświadczenia z Eclipse, dla IntelliJ to generuje pliki *.iml w odpowiednich miejscach, opisujące strukturę projektu i jego zależności w ten sam sposób co Gradle (mamy dzięki temu 2 spójne modele przeznaczone do różnych celów).

IDE, a zależności

Samo dodanie zależności do build.gradle nie wystarczy, żeby IDE to zauważyło - zazwyczaj musimy ręcznie je odświeżyć. Między różnymi IDE, a nawet między wersjami IDE robi się to różnie. W IntelliJ jest to zazwyczaj kwestia kliknięcia "refresh" w odpowiedniej wtyczce. Jeśli to nie zadziała, to możemy ponownie wydać powyższe polecenie - Gradle nadpisze wtedy pliki projektu tak, żeby zależności były już zaktualizowane.

Wskazówki i dalsza lektura

Wersja

Wrapper jest wygenerowany dla wersji 1.12. Jakiś czas temu wyszło Gradle 2 - w tym momencie zmieniło się troszkę API i niektóre elementy nie są wstecznie kompatybilne. Jako, że wersja 2 jest jeszcze dość młoda, to proponuję na razie zostać przy 1.12.

Taski

Wykonanie taska X na poziomie całego projektu sprowadza się de facto do wykonania tego taska dla wszystkich modułów. To znaczy, że na przykład

gradle clean

spowoduje wykonanie:

gradle g419-corpus:clean
gradle g419-external-dependencies:clean
...

a na końcu wyczyszczenie katalogu <root repo>/build (katalogami build w modułach zajmą się taski na poziomie modułów).

W ramach jednego polecenia możemy wywołać więcej niż jeden task, dla różnych modułów, na przykład

gradle clean g419-liner2-api:build g419-liner2-cli:run ...

spowoduje kolejno wykonanie:

gradle clean
gradle g419-liner2-api:build
gradle g419-liner2-cli:run

Dla mniejszych projektów i modułów warto używać pary "clean build" zamiast samego "build" - mamy wtedy pewność, że wszystko buduje się od zera poprawnie; dla większych projektów rekompilacja może trwać trochę za długo.

Do doczytania

Nie znalazłem lepszego tutoriala do Gradle niż ten na ich stronie: https://www.gradle.org/docs/current/userguide/userguide.html

Jeśli kogoś zainteresowało Groovy - nie radzę czytać tutoriali na stronie Codehaus. Ekipa, która tworzy ten język robi świetne rzeczy, ale w kwestii dokumentacji użytkownika zostali sporo w przeszłości. Można spokojnie ufać ich Javadocsom - one są aktualizowane na bieżąco, ale tutoriale są jeszcze dla wersji w okolicach 1.8 (w tej chwili najświeższa jest wersja 2.3, a 2.4 jest w becie), a od tego czasu BARDZO dużo się pozmieniało.
Książka "Groovy in Action" pokrywa starą wersję - niedługo ma wyjść edycja pokrywająca Groovy 2.+, ale to jest jeszcze w MEAP. Zdarzyło mi się mieć do niej dostęp i jest bardzo dobrze napisana.
Jestem w stanie w jakimś stopniu pomóc z nauką tego języka - w razie czego proszę się kontaktować ;)