Strumenti di build e test con Groovy

II parte: Introduciamo il Build by Convention con Gradledi

In questo articolo, il secondo dedicato agli strumenti di build con Groovy, parleremo di Gradle, un tool che unisce alla potenza di Ant una serie di caratteristiche più evolute, come il pattern "build by convention", la gestione delle dipendenze e la gestione di build di progetti multimodulo.

Introduzione

Nell'articolo precedente abbiamo visto Gant, uno strumento che permette di utilizzare Groovy  come linguaggio per definire processi di build con Ant.

In questo articolo viene presentato Gradle, un tool che oltre a permettere una più agevole configurazione dei task di Ant, aggiunge molte caratteristiche evolute, come la possibilità di utilizzare il pattern "build by convention", la gestione delle dipendenze o la gestione di build di progetti multi modulo. Il cuore di Gradle, come già per Gant, è costituito da Ant, utilizzato attraverso l'AntBuilder fornito da Groovy. Oltre a Ant, Gradle si appoggia anche a Ivy, uno strumento molto sofisticato per la gestione delle dipendenze di un progetto.

Le caratteristiche principali di Gradle sono:

  • linguaggio per definire task e dipendenze tra loro;
  • possibiltà di utilizzare direttamente i task di Ant attraverso l'AntBuilder;
  • approccio "convention over configuration", simile a Maven, ma con maggior flessibilità;
  • fasi di configurazione e di esecuzione distinte attraverso l'uso di DAG (Directed acyclic graph);
  • gestione delle dipendenze basato su Ivy;
  • possiblità di utilizzare le infrastrutture di Maven o Ivy già esistenti (p.e. i repository o i file pom.xml);
  • gestione di build multiprogetto totali e parziali;
  • DSL Groovy per la definizione dei file di configurazione;
  • un Wrapper per eseguire Gradle in macchine dove non è installato.

Hello World con Gradle

Prima di analizzare nel dettaglio tutti gli aspetti di Gradle, vediamo un esempio che mostra le sue caratteristiche principali. L'installazione è molto semplice: dopo aver scaricato e estratto lo ZIP, è sufficiente configurare la variabile di ambiente GRADLE_HOME e aggiungere la sottodirectory bin di Gradle al path.

La versione di Gradle usata per questo esempio è la 0.6.

Per sfruttare le convenzioni di Gradle, è necessario creare una struttura di progetto standard, che ricalca quella di Maven. Creiamo quindi una struttura di questo tipo:

projectDir
    src
        main
            java
            resources
        test
            java
            resources

Dentro a "src/main/java" creiamo una classe contenente un metodo main:

package org.daviderossi.esempiogradle;
class GradleMain { 
    public static void main(String args[]) {
        System.out.println("Hello World!!!");
    }
}

Nella root del progetto creiamo poi un file build.gradle contenente queste istruzioni:

usePlugin 'java'
version = '1.0'

Da linea di comando entriamo nella root del progetto e lanciamo

gradle libs

Notiamo che è stata creata una nuova directory "build". Qui troviamo una sottodirectory "classes" che contiene la nostra classe compilata, e una "libs" che contiene il JAR prodotto. E tutto senza configurare niente!

A questo punto facciamo qualche modifica alla nostra classe in modo da utilizzare qualche dipendenza esterna: importiamo quindi i package di Spring Core e Apache Commons Collections. Aggiungiamo poi una classe di test che utilizza jUnit nella directory "main/test/java".

Dobbiamo configurare Gradle in modo da scaricare queste dipendenze per noi. Aggiungiamo allora al file build.gradle le seguenti righe:

repositories {
    mavenCentral()
}
dependencies {
    compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
    compile "org.springframework:spring:2.5.6"
    testCompile "junit:junit:3.8.2"
}

Cancelliamo la directory "build" con il comando

gradle clean

e rilanciamo il comando "gradle libs": vediamo che Gradle cercherà di collegarsi al repository standard di Maven che è http://repo1.maven.org/maven2 e di scaricare le librerie necessarie per la compilazione e per i test.

Concludiamo questo breve esempio creando una distribuzione contenente il JAR prodotto e il contenuto della cartella "src".

Aggiungiamo allora questo task:

task createZip(type: Zip) {
    fileSet(dir: 'src')
    fileSet(dir: 'build') {
        include("**/*.jar")
    }
}

e lanciamo il comando

gradle dists

All'interno della directory "build" troveremo una nuova sottodirectory "distributions" contenente il file ZIP prodotto, con all'interno il JAR e le directory "main" e "test" di "src".

Il file gradle.build completo è il seguente:

usePlugin 'java'
sourceCompatibility = 1.5
targetCompatibility = 1.5
version = '1.0'
manifest.mainAttributes(
    'Implementation-Title': 'Esempio Gradle',
    'Implementation-Version': version,
    'Main-Class': 'org.daviderossi.esempiogradle.GradleMain'
)
repositories {
    mavenCentral()
}
dependencies {
    compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
    compile "org.springframework:spring:2.5.6"
    testCompile "junit:junit:3.8.2"
}
task createZip(type: Zip) {
    fileSet(dir: "src")
    fileSet(dir: "build") {
        include('**/*.jar')
    }
}

Vediamo quindi che, seguendo lo standard di Gradle, con poche righe possiamo gestire le fasi più importanti di un processo di build. Nel resto dell'articolo vedremo in dettaglio il funzionamento di Gradle e come modificare i valori predefiniti.

I task di Gradle

Il core di Gradle è costituito dai task. Un task può essere definito in questo modo:

task helloWorld << {
    println 'Hello world!'
}

Una volta definito, si può far riferimento al task semplicemente con il suo nome.

I task possono avere dipendenze tra di loro, e queste dipendenze determinano l'ordine di esecuzione.

task helloPlanet(dependsOn: helloWorld) << {
    println 'Hello Planet!'
}

In questo caso il task "helloWorld" sarà eseguito prima di "helloPlanet".

Come si può notare, i task di Gradle sono simili ai target di Ant, anche se permettono una maggiore flessibilità. È possibile, ad esempio, eseguire del codice Groovy qualsiasi all'interno di un task, aggiungere dipendenze da task non ancora definiti (caratteristica utile per build multiprogetto), oppure creare dinamicamente task e manipolare task esistenti. Possiamo quindi aggiungere dipendenze anche dopo aver definito un task, aggiungere comportamenti (ad esempio ci sono metodi legati al ciclo di vita dei task , come "doFirst" o "doLast", o alla fase di esecuzione del processo, come "build.afterProject" o "tasks.whenTaskAdded") oppure aggiungere dinamicamente proprietà ai task.

Ad esempio, è possibile aggiungere una dipendenza a un task già definito in questo modo:

task helloPlanet() << {
    println 'Hello Planet!'
}
helloPlanet.dependsOn helloWorld

A differenza di Ant, in Gradle è possibile definire più di un task di default, usando l'istruzione:

defaultTasks 'clean', 'compile'

Una caratteristica interessante è quella di avere fasi di configurazione e di esecuzione distinte: questo permette di avere build condizionali dipendenti dal grafo completo dei task, come nel seguente esempio:

build.taskGraph.whenReady {taskGraph ->
    if (taskGraph.hasTask(':release')) {
        version = '1.0'
    } else {
        version = '1.0-SNAPSHOT'
    }
}
task distribution << {
    println "We build the zip with version=$version"
}
task release(dependsOn: 'distribution') << {
    println 'We release now'
}

Se si lancia il task "distribution" la proprietà "versione" assumerà valore '1.0-SNAPSHOT'; se si lancia 'release' '1.0'.

Quello che si può osservare è che il fatto di eseguire il task "release" ha effetto prima che il task sia effettivamente eseguito.

Build by Convention attraverso i Plugin

I task costituiscono l'elemento principale di ogni processo di build con Gradle. Nella maggior parte dei casi però, come abbiamo visto nell'esempio precedente, non è necessario scrivere nuovi task, ma è possibile sfruttare il pattern "Convention over Configuration" usato da Gradle.

Questa strategia di design ha come obbiettivo quello di diminuire il numero di decisioni (più o meno arbitrarie) che gli sviluppatori si trovano a dover prendere durante lo sviluppo di una applicazione. Ciò è reso possibile utilizzando una serie di convenzioni relative a standard di codifica, layout di progetto e naming: aderendo a questi standard dettati dal framework viene drasticamente ridotto il numero degli elementi che vanno configurati manualmente. Ovviamente, una strategia simile è tanto più efficace quanto più è possibile adattarla alle proprie necessità.

Il primo framework a introdurre questo pattern nel processo di build, e tuttora il più usato, è stato Maven, a cui Gradle rimanda sotto vari aspetti.

Gradle fornisce questa funzionalità attraverso l'uso di "plugin". Con la distribuzione di Gradle vengono forniti una serie di plugin che coprono le necessità principali dei processi di build per progetti Java e Groovy; oltre a quelli predefiniti è possibile poi creare dei plugin personalizzati per rispondere alla esigenze più diverse.

I servizi forniti dai plugin sono di due tipi: aggiungono una serie di task predefiniti (creando dipendenze tra di loro in modo da garantire il corretto ordine di esecuzione), e aggiungono un cosiddetto "Convention object" alla configurazione del progetto. Questo oggetto stabilisce i valori di default per la configurazione, e fornisce un sistema semplice per modificare questi valori, attraverso una serie di proprietà legate ai singoli task. Un esempio chiarirà meglio questo concetto. Come vedremo con maggior dettaglio in seguito, il plugin di Java aggiunge un task "processResources" che permette di copiare delle risorse da una directory a un'altra. La destinazione predefinita è indicata dalla proprietà "destinationDir" del task e ha valore "build/classes". Supponiamo di voler cambiare questo valore. Possiamo cambiare direttamente la proprietà nel task, con questa istruzione:

processResources.destinationDir = new File(buildDir, 'output')

Le risorse saranno adesso copiate nella cartella "output".

In questo esempio abbiamo però modificato la directory di destinazione solo per il task "processResources ". Anche il task "compile" ha una proprietà "destinationDir", che indica dove saranno messe le classi compilate, che ha valore di default "build/classes". Questa proprietà non è stata modificata. Come fare per modificare la directory di destinazione per entrambi i task? Qui ci viene in aiuto il Convention object: questo oggetto ha una serie di proprietà che mappano le relative proprietà dei singoli task. Ad esempio, è presente una proprietà  "classesDirName", che mappa le proprietà "destinationDir" dei task "processResources" e "compile". Valorizzando questa proprietà è possibile così cambiare il valore per entrambi i task. Non tutte le proprietà dei task sono mappate in una corrispondente proprietà del convention object, la decisione è lasciata allo sviluppatore del plugin.

I plugin forniti con la distribuzione di Gradle sono:

  • Java plugin: Il principale plugin, fornisce una serie di task per eseguire build di progetti Java
  • Groovy plugin: Estende il Java plugin e aggiunge il supporto per Groovy
  • WAR plugin: Estende il Java plugin aggiungendo il supporto per progetti WEB
  • Jetty plugin: Estende il WAR plugin e permette di lanciare l'applicazione in un server Jetty embedded
  • OSGi plugin: Permette la generazione di un OSGi manifest
  • Report plugin: Aggiunge dei task per generare report riguardo la build
  • Maven plugin: Aggiunge task per interagire con un repository Maven

Analizzeremo questo aspetto nei dettagli nel prossimo articolo.

Conclusioni

Per questa volta ci fermiamo qui. Gradle è un tool semplice da utilizzare ma offre numerose possibilità, non ultima il supporto al pattern "convention over configuration" che abbiamo qui appena introdotto e che finiremo di trattare in maniera dettagliata nel prossimo articolo, unitamente alla gestione delle dipendenze e la gestione di build di progetti multimodulo.

Da notare come la release utilizzata sia la 0.6, ma lo sviluppo procede velocemente e le nuove versioni vengono rilasciate a intervalli di pochi mesi: ovviamente ci sono gli inevitabili problemi di compatibilità, ma la rapida evoluzione del progetto porterà presto a funzionalità complete e maggiore stabilità.

Riferimenti

[1] Homepage di Gradle

http://www.gradle.org/

[2] Apache IVY

http://ant.apache.org/ivy/

[3] Convention over Configuration Pattern

http://softwareengineering.vazexqi.com/files/pattern.html

 

 

 

 

Condividi

Pubblicato nel numero
141 giugno 2009
Davide Rossi si è laureato in Ingegneria informatica Presso il Politecnico di Milano. Si occupa di tecnologie Java dal 2003 e ha partecipato come consulente a vari progetti in ambito finanziario, marketing e di eCommerce.
Articoli nella stessa serie
Ti potrebbe interessare anche