Dopo aver visto il mese scorso un esempio di build file con le operazioni più comuni per i progetti JavaEE, proseguiamo in questo articolo parlando di come affrontare la build dei progetti di grandi dimensioni. In particolare verrà mostrato un esempio di come realizzare meccanismi di ereditarietà tra build file per facilitare la manutenibilità ed estendibilità della build con ant nei contesti “enterprise”.
Monolithic buildfile vs Cascading buildfile
Quando si ha a che fare con la build di progetti di grandi dimensioni composti da diversi sottoprogetti si possono seguire due strade.
La prima considera il progetto come un blocco monolitico e prevede l‘uso di un solo buildfile (monolithic buidfile) per eseguire tutta la build. Il file di build unico definisce tutti i target necessari ai vari sottoprogetti, permettendo di centralizzare la gestione delle loro dipendenze e allo stesso tempo consentendo la definizione per ciascun sottoprogetto di target specifici per le operazioni di packaging e di deployment.
Questa soluzione facilita le attività di manutenzione sul build file, ma comporta un forte accoppiamento in quanto ogni volta si effettua la build di tutti i moduli.
Per questo motivo l‘uso di un monolithic buidfile è consigliabile nei casi in cui i progetti sono fortemente correlati e presentano complesse dipendenze.
L‘altra strada consiste nel ricorrere a un set di build file (cascading buildfile), uno per ogni sottoprogetto. Si ribalta la prospettiva rispetto a prima, ovvero è possibile effettuare la build di una sola applicazione, senza preoccuparsi delle altre.
Il design prevede, infatti, un master build file nel direttorio radice che invoca i target dei vari build presenti nelle sottodirectory: gli sviluppatori che vogliono fare la build di un singolo progetto si limitano ad usare il build file nella directory relativa.
Questo tipo di soluzione è, quindi, preferibile per i grandi progetti ben definiti e composti da una serie di moduli a sà© stanti.
Approccio tradizionale
La build strutturata con cascading build file è molto diffusa nei progetti e nei framework del mondo Java. Bisogna precisare, tuttavia, che tale approccio deriva dai grandi progetti Unix, primo fra tutti il kernel di Linux, che è un esempio di cascading build realizzata con dei make file.
La tecnica consiste nello strutturare i sottoprogetti in una serie di directory, definendo un master build file nella directory radice che invoca gli altri usando il task
Di seguito è riportato un esempio di master build file che esegue la build di 5 sottoprogetti:
In pratica nel master sono definiti dei target “proxy” che modellano le dipendenze nei vari sottoprogetti.
Ciò produce l‘indubbio vantaggio di poter riusare dei build file contenenti target per operazioni standard (si pensi ad esempio alle operazioni del ciclo di sviluppo compile, build, dist, deploy), consentendo la definizione di librerie di build file.
Un‘altra importante funzionalità offerta dal task
- se inheritall vale true, tutte le properties impostate nel master build file sono ereditate dai file di build dei sottoprogetti;
- se inheritall vale true e ci sono delle properties ridefinite nel task
, queste hanno la meglio su quelle del master build file; - se inheritall vale false, solo eventuali properties definite nel task
del master build file sono ereditate dai file di build dei sottoprogetti; - indipendentemente dal valore di inheritall, le proprietà definite dalla riga di comando (p.e. ant –Dnome.proprietà =”…” nomeTask) vengono sempre passate ai sottoprogetti e non possono essere sovrascritte dalle properties definite nel task
.
È importante sottolineare che il valore di default dell‘attributo inheritall è true. Quindi i sottoprogetti devono usare nomi univoci per le proprietà , altrimenti il comportamento di default prevede, in caso di omonimie, l‘overriding con i valori definiti nel master build file.
Un simile discorso vale per i riferimenti ai percorsi in un progetto. Un build file master può definire attraverso il task
In questo caso però il valore di default di inheritRefs è false, per cui a meno di una sua definizione a true nel task
L‘overriding delle properties e la possibilità di definire delle librerie di build file rendono i cascading build file estremamente flessibili, ma problematici dal punto di vista dell‘estendibilità e della manutenibilità .
Una modifica sulle operazioni di processo, come ad esempio l‘inserimento di target per il controllo della qualità del codice, o semplicemente la modifica dell‘ordine di precedenza, si traduce, infatti nella modifica del build file di ciascun progetto.
Ereditarietà tra buildfile
La comparsa del task
Con il task overriding si può, infatti, centralizzare tutta la gestione del processo all‘interno di un unico file, diverso dal build file di progetto: una scelta di questo tipo aumenta la mantenibilità del build file, garantisce che tutti i progetti seguano le stesse politiche di processo, e crea una separazione netta tra le operazioni di carattere generale e quelle specifiche per i diversi progetti.
Da un punto di vista implementativo, la centralizzazione di tali operazioni è, inoltre, spesso dettata dalla similitudine delle varie applicazioni in termini infrastrutturali, tecnologici e architetturali: è possibile, infatti, definire un insieme di operazioni “generico”, in cui la valorizzazione di opportuni parametri permette la specializzazione rispetto ad uno specifico progetto.
La soluzione che si vuole proporre prevede la definizione di un file “generico”, chiamato MainBuild.xml, in cui sono definiti i target che implementano le diverse fasi del processo e le loro rispettive dipendenze. Questo file generico viene “specializzato” da un file specifico per il progetto, che avrà il classico nome Build.xml.
Figura 1 – Struttura dei build file
Con una struttura di questo tipo si centralizza la definizione delle operazioni inerenti le fasi del processo all‘interno di un unico file, il MainBuild.xml, facilitandone l‘estensibilità e la mantenibilità . Qualora si decidesse di cambiare le fasi di processo, ad esempio aggiungendo dei controlli aggiuntivi sulla qualità del codice o imponendo un certo livello di documentazione, si dovrà modificare un unico file per essere certi che tutti i progetti rispettino le nuove regole.
Attraverso il meccanismo di import, tutti i target definiti nel file importato (MainBuild.xml) sono disponibili all‘interno del file importante (Build.xml): in questo modo è possibile invocare i target come se fossero dichiarati localmente, evitando il riferimento esplicito come nel task
Si consideri ad esempio il seguente target definito nel MainBuild.xml:
... funzionalità generali ...
Un esempio di codice per una estensione funzionale in un Build.xml è il seguente:
... task aggiuntivi ...
In questo modo, il target ridefinito avrà tutte le funzionalità del target precedente, che verrà invocato dalla chiamata nel vincolo di dipendenza, ed in più eseguirà tutti i task aggiuntivi inseriti nella nuova definizione. così facendo si mantiene la separazione tra la definizione delle funzionalità generali e funzionalità specifiche, mantenendo l‘interfaccia comune.
Da un punto di vista implementativo tale tecnica si concretizza nella modifica di uno scheletro del file Build.xml già opportunamente predisposto.
Le modifiche necessarie sono di tre tipi:
- Assegnamento del corretto valore alle properties già definite.
- Definizione delle properties e dei target specifici per il progetto, che non possono essere definiti nel build file generico.
- Estensione, tramite ridefinizione, di target già definiti in MainBuild.xml.
Ovviamente le operazioni di modifica e customizzazione devono essere effettuate con una certa attenzione: la parametrizzazione deve riguardare solo il valore delle properties, e ovviamente non il nome, pena il mancato funzionamento della struttura.
Di seguito è mostrato un frammento di un Build.xml che segue questo tipo di design:
...
depends="MainBuild.init">
overwrite="true"/>
overwrite="true"/>
...
Conclusioni
Nell‘articolo si è parlato di come usare ant per affrontare la build dei progetti di grandi dimensioni. Partendo dalla soluzione tradizionale (cascading build file), che si appoggia al task
La soluzione proposta prevede un MainBuild.xml che centralizza le operazioni di processo e una serie di Build.xml che lo specializzano, aggiungendo le funzionalità specifiche dei vari sottoprogetti.
Riferimenti
[ANT] Sito ufficiale del progetto Jakarta Ant
http://jakarta.apache.org/ant
[ADG] Jessy Tilly – Eric Burke, “Ant Definitive Guide”, Ed. O‘Reilly
[JDA] Eric Hatcher – Steve Loughran, “Java Development with Ant”, Ed. Manning