Per cominciare la serie sugli strumenti di build e test legati a Groovy, in questo articolo vedremo cos‘è Gant, qual è la sua relazione con Ant e quali sono i vantaggi di questo tool.
Introduzione: semplificare il processo di build con Gant
Sviluppando progetti complessi è quasi inevitabile utilizzare degli strumenti automatici per la compilazione del codice, l’import delle librerie, la produzione di JAR, WAR o EAR, e cosi via. I tool più diffusi per questo scopo sono Ant e Maven, sui quali in passato sono già apparsi svariati articoli di MokaByte.
In particolare, Ant è senza dubbio il tool di build più diffuso nel mondo Java, costituendo di fatto uno standard riconosciuto ovunque. Ant è uno strumento estremamente potente e flessibile, ma l’utilizzo di XML come linguaggio di configurazione rende spesso questa potenza difficile da gestire, richiedendo una notevole specializzazione nel processo di build, e rendendo necessario a volte l’utilizzo di estensioni esterne (ad esempio AntContrib) o di custom task. Lo stesso Davidson, creatore di Ant, dice:
“Con il passare del tempo il formato XML è diventato sempre più gravoso per riuscire a realizzare qualcosa di significativo […] Per molto tempo ho pensato che il modo per diminuire il carico per Ant fosse usare un qualche linguaggio di scripting come interfaccia al modello a oggetti dei task di Ant.”
Il problema principale è dovuto al fatto che l’XML è stato concepito come linguaggio di definizione dati, e l’utilizzo come linguaggio di programmazione porta allo sviluppo di codice poco leggibile e non mantenibile oltre che verboso e poco efficiente.
L’uso di un linguaggio di programmazione vero e proprio, come Groovy, permette di semplificare questo processo, lasciando comunque allo sviluppatore la libertà di utilizzare quello che già conosce di Ant e del processo di build.
Cos’è Gant?
Gant è un tool di build che ricalca da vicino Ant e permette di definire i file di configurazione utilizzando Groovy invece di XML. Il cuore di Gant è l’AntBuilder, una utility class di Groovy che consente di richiamare dei task di Ant direttamente da uno script. Gant quindi non utilizza un approccio diverso da Ant, ma costituisce un “wrapper” di Ant, al quale sono delegate le esecuzioni dei task. Sul sito del progetto viene definito cosi il rapporto tra Gant e Ant:
“Benche’ possa essere visto come un concorrente di Ant, Gant utilizza task di Ant per molte delle sue azioni: cosi Gant è in realtà un modo alternativo di realizzare processi di build usando Ant, ma utilizzando un linguaggio di programmazione al posto dell’XML per definire la configurazione.”
L’AntBuilder
Prima di vedere come è realizzato un file di configurazione di Gant, vediamo l’AntBuilder, lo strumento che costituisce il motore di Gant.Groovy mette a disposizione questa classe che permette di richiamare in uno script un qualsiasi task di Ant senza la necessità di utilizzare il linguaggio XML. Ovviamente è necessario conoscere almeno le basi della sintassi di Groovy, peraltro molto simile a Java.
Gli attributi di un task Ant sono passati come parametri al task definito dall’AntBuilder, mentre il contenuto dei task viene messo direttamente all’interno di una closure passata come parametro. Ad esempio, questo script compila e esegue un file Java presente nella stessa directory:
def ant = new AntBuilder() ant.echo('Compiling and executing Temp.java') ant.javac(srcdir:'.', includes:'Temp.java', fork:'true') ant.java(classpath:'.', classname:'Temp', fork:'true') ant.echo('Done')
Questo script sposta il file “resources.properties” dalla directory “origin” alla directory “destination”.
def ant = new AntBuilder() ant.copy(todir: 'destination') { fileset(dir: 'origin') { include(name: "resources.properties") } }
In entrambi i casi si può notare che i parametri non sono passati in modo posizionale ma utilizzando i cosiddetti “named parameters”.
Il file build.gant
Gant costruisce una facciata per l’AntBuilder, permettendo di utilizzarlo in modo trasparente. Con poca fantasia, ma grande chiarezza, il file predefinito per definire una build con Gant è il file “build.gant”, l’esatto equivalente del file “build.xml” di Ant. Dopo aver installato Gant ( si tratta semplicemente di scompattare un file e impostare una variabile d’ambiente), per eseguire una build è sufficiente spostarsi nella directory dove è contenuto il file build.gant e lanciare il comando “gant”: viene eseguito, come per l’analogo comando “ant”, il target di default dello script.
Vediamo le caratteristiche principali di un file build.gant. Un target di Gant è l’equivalente di un target di Ant, e ha una struttura di questo tipo:
target (nome-target : 'Descrizione') { codice groovy }
Il contenuto di un target può essere formato da codice Groovy qualsiasi, quindi è possibile definire variabili, eseguire cicli, definire strutture di controllo, richiamare task predefiniti di Ant o altri target.
La descrizione è obbligatoria, ma può anche essere una stringa vuota; con una descrizione vuota un task non può essere eseguito direttamente da linea di comando: in pratica la descrizione può servire per distinguere i target “pubblici” da quelli “privati”.
È possibile accedere alla descrizione dall’interno di un target usando una variabile fornita da Gant definita dal nome del target seguita da “_description” (ad esempio, se il target si chiama compile: compile_description).
Come per Ant, l’ordinamento dei target può essere definito attraverso delle dipendenze: è fornito un metodo “depends” che può essere richiamato all’interno di qualsiasi target:
target(mioTarget: 'Il mio target') { depends(altroTarget) }
Anche in Gant è possibile specificare un default target. A differenza di Ant, dove il primo elemento di ogni file di build è il nodo “Project” che contiene la definizione del default target, in Gant non esiste una definizione esplicita del progetto. Per esprimere il default target si può semplicemente chiamare un target “default”:
target(default, 'Il target di default') { ... }
oppure si può ricorrere a una definizione abbreviata:
setDefaultTarget(mioTarget)
In Gant è possibile utilizzare uno o più file di properties: tali file possono essere inclusi aggiungendo all’inizio del file l’istruzione
ant.property(file: 'build.properties')
Le proprietà sono poi recuperabili nella mappa “ant.project.properties”. Come in Ant, è possibile passare allo script delle proprietà direttamente da linea di comando in questo modo:
>> gant -Dproprieta1=valore1
Come detto in precedenza, all’interno di un target è possibile richiamare altri target, definiti nello stesso file o in un altro file .gant, oppure richiamare direttamente i task di Ant attraverso l’AntBuilder. Questo grazie al fatto che Gant importa automaticamente l’AntBuilder e lo associa a una variabile chiamata “ant”. Per utilizzare un task è quindi sufficiente chiamarlo come se fosse un metodo dell’oggetto implicito ant:
ant.echo(message: 'Il mio primo task Gant !')
Gant permette di semplificare ulteriormente questo compito molto frequente permettendo di omettere il riferimento all’oggetto “ant”: è possibile quindi richiamare il task precedente scrivendo solamente:
echo(message: 'Il mio secondo task Gant !')
Per importare target da file esterni si usa l’istruzione
includeTargets << new File ( 'mioFile.gant' )
oppure, se il target è precompilato in una classe presente nel classpath di Gant,
includeTargets << gant.targets.Clean
dove “gant.targets.Clean” rappresenta il nome di una classe predefinita di Gant.
Da Ant a Gant
Dopo aver visto gli elementi principali di un file di configurazione di Gant, vediamo alcuni esempi di conversione di target da Ant a Gant. L’applicazione generata è un EAR contenente un WAR e un EJB: vengono qui mostrati alcuni dei target più significativi.
Prima di tutto importiamo un file di properties, e configuriamo il target Clean:
ant.property(file: 'build.properties') // Definisco un alias per la mappa delle proprietà in modo da // accedere alle proprietà con antProperty.nomeProprieta def antProperty = ant.project.properties // Includo il target set Clean e configuro le directory da cancellare includeTargets << gant.targets.Clean cleanDirectory << [ antProperty.path_tmp, antProperty.path_ejb_tmp]
Questo è equivalente al seguente task Ant:
In questo target creiamo alcune directory necessarie a contenere l’EAR finale:
che diventa:
target (prepare_ear: 'Creates the dirs for the EAR') { // Per stampare un messaggio posso usare il task echo di Ant ant.echo(message: 'Creating directories for EAR...' ) // Definisco la dipendenza dal task clean depends( clean ) // Creo le dir con il task di ant mkdir ant.mkdir(dir: antProperty.path_ear) // L'oggetto ant può non essere dichiarato esplicitamente // Utilizzo le GString per valutare le proprietà dentro una stringa mkdir(dir: "${antProperty.path_ear}/WEB-INF") // Posso creare le directory usando Groovy invece di Ant def dir = "${antProperty.path_ear}/WEB-INF/lib" def d1 = new File(dir) d1.mkdir() // Posso utilizzare qualsiasi istruzione Groovy, quindi // posso stampare un messaggio anche con println println 'Directories created' }
Vediamo un esempio di task che crea alcune directory e copia dei file:
^t
diventa
target (prepare_war: 'Creates the directories for the WAR and copies the required files') { depends( prepare_ejb ) println 'Creating directories for WAR:...' ant.mkdir(dir: antProperty.path_war) ant.mkdir(dir: antProperty.path_war_lib) // Groovy permette di avere nomi di proprietà contenenti '.' // chiamandole come stringhe ant.mkdir(dir: "${antProperty.'war.compile.dest.dir'}/WEB-INF/classes") ant.mkdir(dir: "${antProperty.'war.compile.dest.dir'}/WEB-INF/lib") copy(todir: "${antProperty.'war.compile.dest.dir'}/WEB-INF/lib") { fileset(dir: antProperty.'war.compile.lib.dir') { include(name: "**/*.jar") } } copy(todir: antProperty.'war.compile.dest.dir') { fileset(dir: antProperty.'war.compile.webroot.dir') { exclude(name: "**/*.keep*") exclude(name: "**/*.class") exclude(name: "**/web*.xml") } } copy(todir: "${antProperty.'war.compile.dest.dir'}/WEB-INF/lib") { fileset(dir: antProperty.'ejb.library.dir') { include(name: antProperty.'ejb.jar.file') } } copy(todir: "${antProperty.path_war_lib}") { if (antProperty.'environment.server.name' == 'jboss') { println "Copying jars for Jboss" fileset(dir: antProperty.'jboss.library.dir') { include(name: '**/*.jar') } } else { println "Copying jars for other AS" fileset(dir: antProperty.'ws.library.dir') { include(name: '**/*.jar') } } } println 'done' }
Possiamo notare che nel target Gant è possibile usare l’if all’interno del task “copy”, cosa negata invece dalla rigidezza dell’XML.
Vediamo come compilare le classi Java che formano il WAR:
destdir="${war.compile.dest.dir}/WEB-INF/classes" deprecation="yes" fork="no">
diventa
target (compile_war: 'Compiles the WAR') { depends(package_ejb) println "Compiling the WAR archive..." // Definiamo un avariabile che contiene il calsspath def war_compile_classpath = ant.path { fileset(dir: antProperty.'ejb.library.dir') { include(name: antProperty.'ejb.jar.file') } fileset(dir: antProperty.'war.compile.lib.dir') { include(name: "**/*.jar") } } ant.javac( srcdir: antProperty.'war.compile.src.dir', destdir: "${antProperty.'war.compile.dest.dir'}/WEB-INF/classes", fork: 'no', nowarn: 'on', classpath: war_compile_classpath ) { include(name: "**/*.java") } println "done compiling WAR" }
Vediamo infine come generare il WAR:
webxml="${war.compile.webroot.dir}/WEB-INF/web.xml"> includes="web.xml" />
diventa
target(package_war: 'Packages the WAR archive') { depends(compile_war) println "Packaging WAR archive..." ant.war( destfile: "${antProperty.'war.homedir'}/${antProperty.'war.file'}", webxml: "${antProperty.'war.compile.webroot.dir'}/WEB-INF/web.xml" ) { fileset(dir: "${antProperty.'war.compile.dest.dir'}") webinf(dir: "${antProperty.'war.compile.webroot.dir'}/WEB-INF", includes: "web.xml") } println "done packaging WAR" }
Ant o Gant ?
A questo punto, dopo aver visto le caratteristiche principali di Gant, è opportuno farsi qualche domanda. In quali occasioni conviene utilizzare Gant invece di Ant? Conviene convertire i file di Ant già esistenti in file Gant?
Abbiamo visto che il vantaggio principale di Gant è la possibilità di poter utilizzare tutta la flessibilità di Groovy (e quindi di Java…) nella definizione dei file di build. Appare evidente quindi che le occasioni nelle quali conviene scegliere Gant sono quelle in cui il processo di build richiede una logica complessa. In un esempio abbiamo visto un caso in cui, usando ant-contrib, è stato necessario valutare il valore di una proprietà per decidere da quale directory copiare delle librerie. Questo è un caso molto semplice, ma sono molte le situazioni che richiedono una certa logica nel build dell’applicazione, ad esempio azioni diverse in base al valore di alcune proprietà (del file di properties o passate da linea di comando nel momento di lancio della build), verifica di certe condizioni non legate direttamente al processo ( validità sintattica di un file XML esterno, presenza di un file particolare, ecc.), processamento di più elementi (ad esempio trasformazione di alcuni file in un altro formato).
Si capisce immediatamente che quando nello stesso processo sono presenti più di uno di questi casi, magari innestati uno dentro l’altro, la gestione di un file di build che utilizza XML diventa complicata e confusa.
Un altro punto da non sottovalutare è che Gant, basandosi su script Groovy, permette il refactor dei file, permettendo di spostare i task comuni in file separati riutilizzabili, cosi come di usare variabili, metodi e costanti.
Quanto detto porta naturalmente a individuare anche alcuni casi in cui non ci sono grandi vantaggi nell’usare Gant (o nel convertire un file Ant): quando il progetto non richiede un processo di build molto complesso non è necessaria la flessibilità di Groovy.
Conclusioni
Abbiamo visto che Gant si presenta come uno strumento molto potente e flessibile, che permette di riutilizzare comunque tutte le conoscenze acquisite su Ant. L’uso di un linguaggio di scripting dinamico come Groovy, dotato peraltro di una sintassi molto simile a quella di Java, permette in alcuni casi di semplificare la gestione del processo di build rispetto all’uso di Ant.
Oltre a Gant, va infine segnalato un altro tool basato su Groovy e Ivy, Gradle, che per le sue caratteristiche si può definire non solo un tool di build, ma piuttosto un framework completo (alla Maven) che permette anche di gestire le dipendenze, il ciclo di vita e la suddivisione in moduli di un progetto.
Riferimenti
[1] Home page di Gant
[2] Home page di Groovy
http://groovy.codehaus.org
[3] Groovy AntBuilder
http://groovy.codehaus.org/Using+Ant+from+Groovy
[4] Dierk Konig, “Groovy in Action”, Manning
[5] James Duncan Davidson: Ant dot-Next
http://weblogs.java.net/blog/duncan/archive/2003/06/ant_dotnext.html
[6] Home page di Ant
[7] Martin Fowler, Using the Rake Build Language
http://martinfowler.com/articles/rake.html
[8] Meera Subbarao, Ant or Gant?
http://java.dzone.com/articles/ant-or-gant-part-1?page=0,0
[9] Klaus P. Berg, Groovy-power autometed builds with Gant
http://www.javaworld.com/javaworld/jw-02-2008/jw-02-gant.html?page=1
[10] Ant Contrib
http://ant-contrib.sourceforge.net/
[11] Gradle
[12] Allen Holub, Just Say No to XML
http://www.sdtimes.com/content/article.aspx?ArticleID=29508