Strumenti di build e test con Groovy

III parte: Ancora su Gradle. Gestione delle dipendenze e build di progetti multimodulodi

Il mese scorso ci eravamo fermati parlando della Build by Convention attraverso i Plugin, illustrando la filosofia d‘uso e presentando molto brevemente i plugin forniti con la distribuzione di Gradle. Ripartiamo da lì, dando una descrizione più approfondita di tali plugin, per poi concludere la trattazione di Gradle.

Java Plugin

Il plugin principale di Gradle è il Java plugin, che aggiunge una serie di task per compilare e distribuire progetti Java, oltre a fornire una serie di ulteriori proprietà al Convention Object. Queste proprietà riguardano il layout del progetto, la posizione dei file compilati e degli artefatti prodotti.

Il plugin si aspetta che il progetto abbia una struttura standard, che riprende da vicino quella di Maven. Naturalmente, come detto in precedenza è possibile cambiare la struttura predefinita modificando le corrispondenti proprietà del Convention Object. La struttura delle directory è relativa all'elemento gerarchico superiore. Ad esempio, la directory predefinita per contenere i sorgenti, definita dalla proprietà "srcDirNames", vale di default "main/java", ma è relativa all'elemento superiore, la proprietà "srcRoot" (che di default vale "src"), Quindi il valore effettivo, rispetto alla root del progetto sarà "src/main/java". Questo comportamento permette di operare cambi di struttura multipli valorizzando poche proprietà, ma porta purtroppo un elemento di rigidità. Per questo motivo, Gradle introduce anche una serie di proprietà (inizialmente non valorizzate) completamente libere da ogni struttura predefinita, chiamate "floating directories", che possono essere valorizzate a piacere.

La struttura del progetto e le proprietà del Convention Object sono riassunte nelle tabelle 1 e 2.

Figura 1 - Tabella con la struttura del progetto

 

Figura 2 - Tabella con le proprietà del Convention Object

Vediamo le definizione delle proprietà del convention object:

  • DirectoryName: indicano il nome della proprietà che effettivamente andrà modificata
  • DirectoryFile: indicano la posizione nella gerarchia delle directory. Ad esempio, srcDirs è un valore readonly ralativo sempre a srcRoot, indipendentemente dal valore che entrambi assumono. Quindi andando a modificare la proprieta srcDirNames, noi andremo a modificare quella parte della gerarchia definita da srcDirs.
  • DefaultName: indicano i valori di default
  • DefaultFile: indicano come questi valori operano sulla struttura.

Altre proprietà definite dal plugin sono sourceCompatibility e targetCompatibility (indicano la versione di Java e di default hanno valore 1.5), archivesBaseName (il nome degli archivi generati, e di default hanno il valore del nome del progetto), manifest e metaInf (che di default sono vuote).

Oltre all'oggetto convention, il plugin aggiunge al progetto anche una serie di "configurations" predefinite e le associa ai task: una configuration in pratica rappresenta una specie di "contenitore" delle dipendenze associate a un task. Le principali configuration aggiunte dal plugin sono "compile" (per indicare le dipendenze necessarie per la compilazione), e "runtime" (per le dipendenze necessarie a runtime).

Il plugin aggiunge una serie di task che coprono tutti gli aspetti più comuni del processo di build. I principali sono:

Javadoc

Questo task serve per generare la documentazione di progetto. Non ha nessuna dipendenza da altri task.

Clean

Questo task serve a cancellare i file definiti dalla sua proprietà "dir" che di default è mappata sulla directory "buildDir".

Resources

Questo task ha due istanze, "processResources" e "processTestResources" e serve a copiare le risorse utilizzate dall'applicazione e dai test dalle directory sorgenti alle directory "classesDir" e "testClassesDir".

Compile

Come il task "resources" anche questo task ha due istanze, "compile" e "compileTest". Di default i file compilati vengono messi in "build/classes". Il classpath usato per la compilazione è quello associato alle "configuration" associate ai task dal plugin. Vedremo più avanti come gestire le dipendenze di un progetto. Il task "compile" utilizza il task javac di Ant per la compilazione.

Test

Questo task esegue i test del progetto compilati dal task "compileTest" nella directory "classesTestDir" (di default "build/test-classes"). Internamente utilizza i task junit e testNG di Ant.

Jar e Libs

Il task Jar genera un Jar contenente le classi e le risorse del progetto. Il Jar generato viene messo nella directory "builds/libs" e viene chiamato "NomeProgetto-Versione.jar". Il task libs dipende dal task Jar e genera tutti i Jar e i War del progetto.

Dists

Il task dists esegue tutti i task Jar, War, Zip e Tar del progetto.

Upload[Configuration]

Il task di upload viene usato per caricare un archivio generato dal processo (Jar, War, Zip...) in un repository.

Il convention object del plugin di Java ha anche una proprietà "manifest" che serve a definire gli attributi del file MANIFEST.MF.

Gli altri Plugin

Oltre al Java plugin nella distribuzione di Gradle sono presenti anche altri plugin. Vediamo qui molto brevemente quali sono.

Groovy plugin

Il Groovy plugin estende il Java plugin e permette di gestire le build di progetti contenenti classi Groovy e Java. Questo plugin si aspetta una struttura di progetto uguale a quella standard del Java plugin, al quale vengono aggiunte le due directory "src/main/groovy" e "src/test/groovy" che possono contenere sia classi Groovy che Java.

WAR plugin

Il War plugin estende il Java plugin. Disabilita in automatico il task Jar e aggiunge il task War. Di default i file della Web application devono essere messi nella directory "src/main/webapp". Il plugin fornisce due configurazioni aggiuntive, "providedCompile" e "providedRuntime" che sono analoghe alle configurazioni "compile" e "runtime" del Java plugin ma non aggiungono le dipendenze all'archivio prodotto.

Jetty plugin

Il Jetty plugin permette di installare e eseguire l'applicazione in una istanza di Jetty embedded. Questo plugin aggiunge i task "jettyRun", "jettyRunWar" e "jettyStop".

Maven plugin

Questo plugin permette di effettuare il deploy in un repository Maven remoto o di installare in un repository locale gli archivi generati dal progetto, generando automaticamente il file pom.xml.
L'integrazione con dei repository di Maven per il download delle dipendenze è automaticamente abilitata in Gradle, e non è quindi necessario utilizzare questo plugin.

Dependency Management

Java non ha mai offerto una soluzione standard per la gestione delle dipendenze di un progetto. Per risolvere questo problema sono nati alcuni tool, come Maven e Ivy, che richiedono la definizione delle dipendenze attraverso file di configurazione XML e che permettono il download delle librerie richieste da repository locali o remoti. Gradle si appoggia a Ivy per la gestione delle dipendenze, ed è quindi totalmente compatibile con Maven e con la sua struttura di repository.

In Gradle le dipendenze sono gestite attraverso i configuration object introdotti dai plugin. È anche possibile definire configurazioni personalizzate. Le configurazioni in pratica permettono di raggruppare le dipendenze e di associarle ai task per cui sono necessarie. Ad esempio il plugin Java aggiunge le configurazioni "compile" e "runtime" (che abbiamo già visto), testCompile e testRuntime (usate per aggiungere le dipendenze usate per compilare e eseguire i test), "archives" (usata per l'upload degli artefatti prodotto in un repository remoto) e "default".

Le dipendenze possono appartenere a due categorie: le "module dependencies" e le "client module dependencies".
Le module dependencies sono le dipendenze che generalmente sono scaricate da un repository remoto e sono definite attraverso un descrittore XML, pom.xml o ivy.xml. Attraverso questi descrittori, Gradle è in grado di determinare l'intero albero delle dipendenze transitive e di scaricarle direttamente dal repository. Se nel repository non è disponibile il file descrittore Gradle scaricherà solamente il Jar richiesto.

Questo tipo di dipendenze si può definire in vari modi:

dependencies {
    runtime group: "org.springframework", name: "spring-core",
                        version: "2.5"
    runtime "org.springframework:spring-core:2.5",
        "org.springframework:spring-aop:2.5"
    runtime(
        [group: "org.springframework", name: "spring-core", version: "2.5"],
        [group: "org.springframework", name: "spring-aop", version: "2.5"]
    )
    runtime("org.hibernate:hibernate:3.0.5") {
        transitive = true
    }
    runtime group: "org.hibernate", name: "hibernate", version: "3.0.5",
                    transitive: true
    runtime(group: "org.hibernate", name: "hibernate", version: "3.0.5") {
        transitive = true
    }
}

Tutte le istruzioni precedenti sono equivalenti, e cercheranno di scaricare i Jar di Spring 2.5 e Hibernate 3.0.5 con le loro dipendenze.

Nel caso si voglia scaricare solo il Jar senza le sue dipendenze si può ricorrere alle seguenti notazioni:

dependencies {
    runtime "org.groovy:groovy:1.5.6@jar"
    runtime group: "org.groovy", name: "groovy", version: "1.5.6", ext: "jar"
}

Le client module dependencies permettono di definire direttamente nel file build.gradle le dipendenze transitive. La notazione è la seguente:

dependencies {
    runtime module("org.codehaus.groovy:groovy-all:1.5.6") {
        dependency("commons-cli:commons-cli:1.0") {
            transitive = false
        }
        module(group: "org.apache.ant", name: "ant", version: "1.7.0") {
            dependencies "org.apache.ant:ant-launcher:1.7.0@jar",
                    "org.apache.ant:ant-junit:1.7.0"
        }
    }
}

In questo caso Gradle non cerca un descrittore per le dipendenze ma scarica direttamente le librerie definite nel file di build. È possibile anche escludere delle dipendenze, così come definire la dipendenza da un altro progetto in una build multiprogetto.

Basandosi su Apache Ivy, Gradle è in grado di gestire varie configurazioni di repository, attraverso l'uso dei "resolvers". In pratica un resolver definisce una strategia di individuazione delle risorse in un repository.

Per configurare il repository principale di Maven (http://repo1.maven.org/maven2) è sufficiente aggiungere:

repositories {
    mavenCentral()
}

È possibile definire dei repository alternativi dove cercare le librerie se non sono disponibili nel repository principale, oppure aggiungere dei repository personalizzati.

Oltre a usare i repository di Maven, Gradle permette di gestire agevolmente il caso in cui le librerie siano in una directory locale, attraverso un FlatDir Resolver:

repositories {
    flatDir name: "localRepository", dirs: "lib"
    flatDir dirs: ["lib1", "lib2"]
}

È possibile poi definire repository personalizzati, utilizzando vari protocolli di accesso (come HTTP, SSH...) e specificando il pattern di individuazione delle librerie creando nuovi resolvers. Ad esempio, per un repository con la struttura di Maven, il pattern utilizzato è il seguente:

root/[organisation]/[module]/[revision]/[module]-[revision].[ext]

Gradle permette anche di effettuare l'upload degli archivi prodotti (Jar, War, Zip...) in un repository remoto attraverso il Maven plugin.

Build Multiprogetto

Per concludere questa panoramica delle caratteristiche principali di Gradle, va ricordato il supporto alle build multiprogetto.
Gradle gestisce sia progetti con layout gerarchico, dove esiste una directory per il progetto principale e i sottoprogetti sono contenuti in sue sottodirectory, sia con layout piatto, dove i sottoprogetti sono contenuti in directory sullo stesso livello del progetto principale. In ogni caso, almeno nella directory principale devono essere contenuti un file chiamato "settings.gradle" che contiene informazioni relative alla struttura del progetto e il file "build.gradle".

Per definire un multi-progetto gerarchico, questo file dovrà contenere questa istruzione:

include "progetto1", "progetto2"

dove progetto1 e progetto2 sono due sottoprogetti contenuti nella stessa directory del progetto principale .
Nel caso progetto1 e progetto2 siano allo stesso livello del progetto root, il file settings.gradle dovrà contenere:

includeFlat "progetto1", progetto2"

La configurazione di tutti i progetti può essere gestita nel file build.gradle principale, oppure ogni sottoprogetto può avere un proprio file di configurazione. Ad ogni modo nel file principale sono automaticamente definite delle proprietà per gestire le configurazioni comuni a tutti i progetti o solo ai sottoprogetti: "allprojects" e "subprojects". Queste proprietà accettano una closure di configurazione, il cui contenuto è delegato a tutti i progetti interessati. Nel file principale è inoltre possibile definire e manipolare task per tutti i sottoprogetti.

Gradle, creando un grafo dei task di tutti i progetti prima dell'esecuzione, riesce a gestire correttamente le build parziali lanciate da un sottoprogetto. Se ad esempio il progetto1 dipende dal progetto2, possiamo definire questa dipendenza nel file build.gradle del progetto root:

project(":progetto1") {
    dependencies {
        compile project(":progetto2")
    }
}

In questo modo è possibile entrare nella directory di progetto1 e lanciare la compilazione: prima di lanciare il task, Gradle genera il Jar di progetto2 e lo mette nel classpath di progetto1.

Il supporto ai processi di build multiprogetto di Gradle non si limita a queste caratteristiche, ma offre molto di più.
Naturalmente anche i processi di build multiprogetto sono liberamente configurabili. Per la loro gestione sono richiesti strumenti in grado di semplificare il più possibile il lavoro, rimanendo nello stesso tempo abbastanza flessibili da adattarsi ai diversi casi.
Il supporto di Gradle, grazie all'utilizzo del pattern di "build by convention" e alla possibilità di configurare  in modo semplice ogni aspetto del processo, soddisfa efficacemente entrambi questi requisiti.

Conclusioni

Come abbiamo visto, Gradle si presenta come un tool semplice da utilizzare ma che offre numerose possibilità. Grazie al supporto al pattern "convention over configuration" offerto dai plugin, è possibile eseguire le operazioni più comuni del processo con poche semplici istruzioni, ma la possibilità di configurare ogni aspetto fornisce comunque la flessibilità necessaria per gestire agevolmente tutti i casi.
Gradle è un progetto giovane e in rapida evoluzione. La release utilizzata è la 0.6, ma lo sviluppo procede velocemente e le nuove versioni vengono rilasciate a intervalli di pochi mesi. Come tutti i progetti giovani soffre alcuni problemi, sopratutto per quanto riguarda la compatibilità tra versioni diverse e l'implementazione incompleta di alcune funzionalità. Tuttavia, Gradle è considerato dai suoi autori  abbastanza maturo per l'uso: c'è ancora molto da fare, ma quello che c'è si può considerare stabile.

Riferimenti

[1] Home Page 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
142 luglio 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