Mimo, że PHP jest językiem interpretowanym a kod PHP nie wymaga kompilacji, to developerzy przeprowadzają np. generację lub transformację kodu autoloadera. Doskonałym narzędziem służącym do zautomatyzowania procesu budowy oprogramowania jest Apache Ant. To co różni Ant i np. znany z Linuksa Make jest to, że Ant używa plików w formacie XML do opisu procesu budowy i jego zależności, podczas gdy Make ma własny format Makefile. Projekt Ant jest w związku z tym przenośny, Make nie. Domyślnie plik XML w Ant nazywa się build.xml.
Rozważmy następujący projekt umieszczony w katalogu /root
:
1 2 3 4 5 6 7 8 9 10 11 12 |
. ├── build │ ├── phpcs.xml │ ├── phpmd.xml │ ├── src_autoload.php.in │ └── tests_autoload.php.in ├── build.xml ├── phpunit.xml.dist ├── src │ └── autoload.php └── tests └── autoload.php |
Katalog build zawiera pliki konfiguracyjne XML dla PHP_CodeSniffer (phpcs.xml), PHPMD (phpmd.xml). PHP_CodeSniffer i PHPMD to narzędzia do statycznej analizy kodu PHP, którym przyjrzymy się bliżej w kolejnym artykule o Jenkinsie i PHP, dotyczącym tzw. Continous Inspection (ciągłej inspekcji). Plik phpunit.xml (lub phpunit.xml.dist) to plik konfiguracyjny projektu PHPUnit. Pliki src/autoload.php, tests/autoload.php zawierają kod autoloadera dla aplikacji i zestaw testów.
Plik Ant bulid.xml definiuje tzw. targety czyli cele (zestawy zadań), które mają zostać wykonane. Podczas uruchamiania Ant istnieje możliwość wybrania, które targety mają zostać uruchomione. Jeżeli nie jest podany żaden target to wykonywany jest domyślny projekt. Każdy target może zależeć od innego targetu, Ant rozpoznaje zależności. Zadanie to kod (np. skrypt), który ma być wykonany.
Poniższy tekst pochodzi z tej strony i jest nieco przeze mnie zmodyfikowany. Umieszczam tutaj ten tekst gdyż stanowi on bardzo dobre wprowadzenie do Apache Ant.
Wstęp
Apache Ant jest narzędziem wspomagającym i automatyzującym proces kompilacji. Umożliwia definiowanie skryptów za pomocą języka XML, które to skrypty wykonują zadania. Cytując dokumentację:
Apache Ant is a Java-based build tool. In theory, it is kind of like make, without make’s wrinkles.
Można zatem założyć, że Ant powinien być pierwszym narzędziem jakie będzie nam służyć w trakcie kompilowania naszych programów.
Strona domowa projektu: http://ant.apache.org
Struktura projektu
Ant jest narzędziem wymagającym stosunkowo prostej struktury projektu. Po ściągnięciu go ze strony należy skonfigurować zmienną ANT_HOME
tak by wskazywała na katalog w którym zainstalowano anta, oraz do zmiennej PATH
dodać ścieżkę $ANT_HOME/bin
(w windowsie: %ANT_HOME%/bin). Następnie sprawdzamy czy wszystko jest prawidłowo skonfigurowane:
1 2 3 |
$ ant Buildfile: build.xml does not exist! Build failed |
Jak widać by utworzyć projekt wystarczy w katalogu umieścić plik build.xml
. Jest to plik XML w którym definiujemy właściwości projektu. Najprostsza jego wersja wygląda tak:
1 2 3 4 |
<?xml version="1.0" encoding="iso-8859-2"?> <project name="Project name" basedir="." default="clean"> <!-- --> </project> |
Gdzie:
name
– nazwa projektu, która nie jest obowiązkowa.
basedir
– ścieżka bazowa od której są liczone wszystkie ścieżki względne. Też nieobowiązkowe, ale domyślnie jest to ścieżka do folderu z plikiem build.xml
.
default
– domyślne zadanie, które będzie wykonane jeżeli nie podamy zadań do wykonania. Od wersji 1.6 biblioteki, każdy projekt posiada domyślnie skonfigurowane wszystkie zadania dostarczone razem z biblioteką.
Jak zatem łatwo zauważyć, ant pozwala na bardzo swobodną konfigurację projektu. Niestety w starszych wersjach biblioteki brak konfiguracji domyślniej wymuszał tworzenie dużej ilości “domyślnego” kodu. Obecnie nie jest to konieczne, ale często trzeba się napisać by odpowiednio skonfigurować projekt. Do prawidłowego działania wystarczy JDK w wersji 1.2, ale producenci rekomendują użycie JDK w wersji 1.5.
Cele i zadania
Projekt w Ant jest podzielony na cele (target), które zawierają zadania (task). Cele definiujemy sami, a zadania są dostarczone wraz z biblioteką, ale można i samemu stworzyć zadanie poprzez rozszerzenie klasy Task.
Target – cel
Cel jest logicznym krokiem w procesie kompilacji. Składa się z zadań i opisuje jakiś etap projektu. Przykładem celu może być kompilacja, umieszczenie na serwerze, wykonanie testów czy też synchronizacja z SVN. W naszym projekcie przygotujemy wszystko od podstaw wykorzystując cele i zadania. Stwórzmy więc katalog z plikiem build.xml
i dodajmy pierwsze zadanie:
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0" encoding="iso-8859-2"?> <project name="Project name" basedir="." default="create"> <description>Tutorial ANT na http;//4programmers.net/java/Ant</description> <!-- ================================= target: create ================================= --> <target name="create" depends="" description="Tworzy strukturę projektu"> </target> </project> |
Cel posiada obowiązkową nazwę i opcjonalny opis. Może też zależeć od innych celów np. tworzenie archiwum jar zazwyczaj musi być wykonane po skompilowaniu kodu. Zależności definiujemy za pomocą atrybutu depends
w którym poddajemy nazwy zależnych celi rozdzielone przecinkiem.
Dodajmy do naszego celu kilka zadań, które będą nam tworzyć strukturę projektu.
Task – zadanie
Zadanie jest pojedynczą operacją wykonywaną by osiągnąć cel. Może być to na przykład stworzenie katalogu, wywołanie kompilatora lub wypisanie czegoś w konsoli. Ilość zadań w ancie jest ogromna. Biblioteka posiada około 70 wbudowanych zadań, a istnieje jeszcze możliwość ściągnięcia dodatkowy bibliotek jak i stworzenia własnego rozwiązania.
Każde zadanie posiada unikalną listę parametrów, które są przekazywane jako atrybuty lub tagi XML. Sposób konfiguracji jest więc dość swobodny. Z jednej strony zapewnia to dużą elastyczność, ale z drugiej wymaga częstego zaglądania do dokumentacji w celu sprawdzenia jak skonfigurować taki czy inny cel. Poniżej nasz plik build.xml
wzbogacony o definicję zadań, po wykonaniu których zostanie stworzona struktura projektu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="iso-8859-2"?> <project name="Project name" basedir="." default="create"> <description>Tutorial ANT na http;//4programmers.net/java/Ant</description> <!-- ================================= target: create ================================= --> <target name="create" depends="" description="Tworzy strukturę projektu"> <echo>Tworzę katalog ze źrodłami</echo> <mkdir dir="./src/pl/koziolekweb/programmers/ant"/> <echo>Tworzę katalog z testami</echo> <mkdir dir="./test/pl/koziolekweb/programmers/ant"/> <echo>Tworzę katalog ze skompilowanymi klasami</echo> <mkdir dir="./bin/classes/"/> <echo>Tworzę katalog ze skompilowanymi testami</echo> <mkdir dir="./bin-test"/> </target> </project> |
Przykładowe zadania wbudowane
W powyższym przykładzie wykorzystaliśmy dwa zadania wbudowane. Pierwsze z nich echo pozwala na wypisanie komunikatu na ekranie. Drugie mkdir tworzy katalogi jeżeli nie istnieją. Zadania wbudowane w większości odpowiadają potrzebom projektów. Do najpopularniejszych należą javac uruchamiające kompilator, jar tworzące archiwum i javadoc służące do generowania dokumentacji. Innymi przydatnymi zadaniami są copy służące do kopiowania plików, delete usuwające pliki i zip pozwalające na pakowanie plików.
Parametryzowanie zadań
Nasze zadania zazwyczaj będą współdzielić niektóre informacje. Są to przede wszystkim dane o ścieżkach z kodem źródłowym, nazwach plików jar/war/ear, ścieżkach do bibliotek. Warto trzymać je w jednym miejscu i mieć możliwość szybkiej ich edycji. Ant udostępnia dwie metody pozwalające na osiągnięcie tego celu. Pierwszą z nich jest możliwość definiowania zmiennych bezpośrednio w pliku build.xml
, a drugą użycie pliku build.properties
. W obu przypadkach odwołanie do wartości zmienne następuje za pomocą ${nazwa_zmiennej}
Zmienne w pliku build.xml
Zmienne możemy definiować w pliku build.xml
za pomocą specjalnego elementu <property/>
. Na przykładzie naszego pliku konfiguracyjnego będzie to wyglądać następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?xml version="1.0" encoding="iso-8859-2"?> <project name="Project name" basedir="." default="create"> <description>Tutorial ANT na http;//4programmers.net/java/Ant</description> <property name="src" value="./src"/> <property name="src.test" value="./test"/> <property name="target" value="./bin/classes/"/> <property name="target.test" value="./bin-test"/> <property name="main.package" value="pl.koziolekweb.programmers.ant"/> <property name="main.package.dir" value="pl/koziolekweb/programmers/ant"/> <!-- ================================= target: create ================================= --> <target name="create" description="Tworzy strukturę projektu"> <echo>Tworzę katalog ze źrodłami</echo> <mkdir dir="${src}/${main.package.dir}"/> <echo>Tworzę katalog z testami</echo> <mkdir dir="${src.test}/${main.package.dir}"/> <echo>Tworzę katalog ze skompilowanymi klasami</echo> <mkdir dir="${target}"/> <echo>Tworzę katalog ze skompilowanymi testami</echo> <mkdir dir="${target.test}"/> </target> </project> |
Metoda ta jest dobra ale jedynie wtedy gdy wartości zmiennych są takie same dla wszystkich osób pracujących przy projekcie. Jeżeli zmienna wymaga różnych wartości w zależności od np. środowiska lub osoby, która uruchamia proces to nie wskazane jest zmienianie jej za każdym razem. Jest to szczególnie uciążliwe w przypadku pracy z systemami kontroli wersji. Z jednej strony nie należy wysyłać do repozytorium pliku z wiadomościami przydatnymi tylko nam, ale z drugiej strony należy posiadać zawsze aktualny plik build.xml
. W takim przypadku należy użyć pliku build.properties
zamiast bezpośrednich wpisów w build.xml
.
Użycie pliku build.properties
Plik ten jest typowym plikiem .properties
ze wszystkimi jego zaletami i wadami (kodowanie US-ASCII, patrz Properties – pliki tekstowe). Przenieśmy część naszych zmiennych do tego pliku:
1 2 |
target=./bin/classes/ target.test=./bin-test/ |
Jak widać nie ma różnicy pomiędzy tymi metodami definiowania zmiennych. Plik build.properties
może być już wyjęty z pod kontroli wersji i nie ma potrzeby zaśmiecania repozytorium różnymi wersjami konfiguracji. wystarczy tylko podlinkować nasz plik w pliku build.xml
i gotowe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?xml version="1.0" encoding="iso-8859-2"?> <project name="Project name" basedir="." default="create"> <description>Tutorial ANT na http;//4programmers.net/java/Ant</description> <property file="build.properties" /> <property name="src" value="./src" /> <property name="src.test" value="./test" /> <property name="main.package" value="pl.koziolekweb.programmers.ant" /> <property name="main.package.dir" value="pl/koziolekweb/programmers/ant" /> <!-- ================================= target: create ================================= --> <target name="create" depends="configure" description="Tworzy strukturę projektu"> <echo>Tworzę katalog ze źrodłami</echo> <mkdir dir="${src}/${main.package.dir}" /> <echo>Tworzę katalog z testami</echo> <mkdir dir="${src.test}/${main.package.dir}" /> <echo>Tworzę katalog ze skompilowanymi klasami</echo> <mkdir dir="${target}" /> <echo>Tworzę katalog ze skompilowanymi testami</echo> <mkdir dir="${target.test}" /> </target> </project> |
Przykłady
Omówię teraz przykładowy plik build.xml
, który zawiera kompletny zestaw najpopularniejszych zadań. Zadania zostały tak przygotowane, że pokrywają najpopularniejsze wymagania dotyczące sposobu dostarczenia kodu. Dla ograniczenia liczby plików wszystkie zmienne zostały umieszczone w pliku build.xml
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
<?xml version="1.0" encoding="iso-8859-2"?> <project name="Project name" basedir="." default="all"> <description>Tutorial ANT na http;//4programmers.net/java/Ant</description> <property file="build.properties" /> <property name="target" value="./bin/" /> <property name="target.classes" value="./bin/classes/" /> <property name="target.test" value="./bin-test/" /> <property name="target.jar" value="./bin/jar" /> <property name="lib" value="./lib" /> <property name="lib.junit" value="${lib}/junit4.jar" /> <property name="reports.tests" value="./bin-test/raport/" /> <property name="src" value="./src" /> <property name="src.test" value="./test" /> <property name="main.package" value="pl.koziolekweb.programmers.ant" /> <property name="main.package.dir" value="pl/koziolekweb/programmers/ant" /> <target name="clean" description="Usuwa katalogi ze skompilowanym kodem"> <delete includeemptydirs="true" failonerror="no"> <fileset dir="${target.classes}" includes="**/*" /> </delete> <delete includeemptydirs="true" failonerror="no"> <fileset dir="${target}" includes="**/*" /> </delete> <delete includeemptydirs="true" failonerror="no"> <fileset dir="${target.test}" includes="**/*" /> </delete> </target> <target name="create" depends="clean" description="Tworzy strukturę projektu"> <mkdir dir="${src}/${main.package.dir}" /> <mkdir dir="${src.test}/${main.package.dir}" /> <mkdir dir="${target.classes}" /> <mkdir dir="${target.test}" /> <mkdir dir="${target.jar}" /> <mkdir dir="${lib}" /> <mkdir dir="${reports.tests}" /> </target> <target name="compile" depends="create" description="kompiluje kod"> <javac srcdir="${src}" destdir="${target.classes}" /> </target> <target name="test-compile" description="kompiluje kod" depends="compile"> <javac srcdir="${src.test}" destdir="${target.test}" classpath="${lib.junit};${target.classes}" /> </target> <target name="run-test" depends="test-compile" description="Uruchamia testy jednostkowe"> <junit> <classpath> <pathelement location="${lib}" /> <pathelement location="${lib.junit}" /> <pathelement path="${target.classes}" /> <pathelement path="${target.test}" /> </classpath> <batchtest fork="yes" todir="${reports.tests}"> <fileset dir="${src.test}"> <include name="**/*Test.java" /> </fileset> </batchtest> <formatter type="xml" /> </junit> </target> <target name="package" depends="compile" description="tworzy plik jar"> <jar destfile="${target.jar}/app.jar"> <fileset dir="${target.classes}" /> </jar> </target> <target name="test-package" depends="test-compile" description="tworzy plik jar z testami"> <jar destfile="${target.jar}/app-test.jar"> <fileset dir="${target.test}" /> </jar> </target> <target name="src-package" description="tworzy plik jar ze źródłami i źródłami testów"> <jar destfile="${target.jar}/app-src.jar"> <fileset dir="${src}" /> </jar> <jar destfile="${target.jar}/app-src-test.jar"> <fileset dir="${src.test}" /> </jar> </target> <target name="javadoc"> <javadoc packagenames="pl.koziolekweb.programmers.ant*" sourcepath="${src}" defaultexcludes="yes" destdir="${target}/docs/api" author="true" version="true" use="true" windowtitle="App API" classpath="${target.jar}/app.jar" /> <javadoc packagenames="pl.koziolekweb.programmers.ant*" sourcepath="${src.test}" defaultexcludes="yes" destdir="${target}/docs/test-api" author="true" version="true" use="true" windowtitle="App tests API" classpath="${lib.junit};${target.test}"/> <zip destfile="${target}/docs/api.zip" basedir="${target}/docs/api/" /> <zip destfile="${target}/docs/test-api.zip" basedir="${target}/docs/test-api/" /> </target> <target name="all" depends="package,test-package,src-package"></target> </project> |
Usuwanie starych plików
Pierwszym celem jest clean. Usuwa on wszystkie stare skompilowane pliki, pliki jar i stare testy. Zadanie delete przyjmuje w tym przypadku listę plików do usunięcia z podanego katalogu, ale bez tego katalogu. W trakcie określania zbioru plików, fileset
, istnieje możliwość stworzenia listy wyłączeń przez zdefiniowanie atrybuty albo elementu excludes
Tworzenie katalogów
Ten cel został już omówiony wcześniej.
Kompilacja źródeł i testów
Te dwa cele reprezentowane przez compile i test-compile są najważniejszymi elementami projektu. Określają, które katalogi zawierają pliki źródłowe i pozwalają na ich kompilację. W przypadku compile proces jest bardzo prosty za pomocą atrybutu srcdir określono katalog źródłowy, a za pomocą destdir docelowy. Kompilacja testów jest trochę bardziej skomplikowana ponieważ wymaga określenia poza katalogiem źródłowym i docelowym też ścieżki z zależnościami. Można wykonać to zadanie na kilka sposobów. Najprostszym jest użycie atrybutu classpath i ręczne dodanie wszystkich potrzebnych elementów. Cel test-compile jest uzależnione od compile ponieważ do uruchomienia testów potrzebne są skompilowane klasy aplikacji.
Uruchomienie testów
Ten cel jest trochę bardziej skomplikowany. Po pierwsze classpath jest określony za pomocą listy elementów pathelement. Pozwala to na budowanie długich ścieżek i na dłuższą, nomen omen, metę jest znacznie bardziej wygodne. Po drugie użyty został element batchtest zamiast test. Pozwala on na konfigurację testów na podstawie ścieżki, a test przyjmuje jako atrybut class pojedyncze klasy testowe.
Tworzenie pakietów
Cele package,test-package i src-package mają za zadanie utworzenie plików jar zawierających odpowiednio aplikację, testy, kod źródłowy aplikacji i kod źródłowy testów.
Tworzenie dokumentacji
Cele javadoc tworzy dokumentację kodu źródłowego i testów. Następnie pakuje ją do plików zip. Przy tworzeniu dokumentacji też należy zdefiniować classpath w przeciwnym wypadku javadoc zwroci błędy podobne do tych jakie zwraca kompilator gdy nie odnajdzie zalezności.
Wszystko naraz
Ostatni cel jest domyślny. Uruchamia wszystkie poprzednie dbają o to by zależności były uruchamiane tylko raz.