Construcción de Proyectos con Gradle

Publicado en

Una parte importante del desarrollo de software es la construcción de los proyectos y los sistemas completos. Desde la clásica herramienta make en los 70’s, ha habido varios mecanismos y sistemas distintos para construcción de proyectos, algunos enfocados a ciertos aspectos de la construcción, otros tratando de abarcar todo el espectro: compilación, manejo de dependencias, integración contínua, automatización del proceso de construcción, etc. En el mundo Java, primero tuvimos Ant, una herramienta en cierta forma similar al make pero hecha 100% en Java, enfocada a construcción de proyectos Java. Al principio parecía muy buena, porque la sintaxis no era tan oscura como la de make, sino que usa XML, de modo que es más entendible lo que se quiere hacer. Pero cuando se comienzan a hacer scripts más complejos, la XML-itis se vuelve difícil de entender y modificar.

Luego hemos tenido otras cosas como Ivy y Maven. Ivy se enfoca al manejo de dependencias, que consiste en indicar qué bibliotecas externas de software se necesitan para poder compilar el proyecto, cuáles se necesitan para correrlo, cuáles para probarlo, etc. Maven, por su parte, abarca el ciclo completo: compilación, manejo de dependencias, pruebas unitarias, documentación técnica (javadoc) y hasta el sitio del proyecto puede hacer, todo esto gracias a que tiene una arquitectura de plugins bastante completa. Pero Maven, al ser tan completo, se vuelve a la vez complejo: para compilar un proyecto tipo “hola, mundo!” y generar su documentación se requieren unas 30 líneas de XML (probablemente más líneas que el mismo código fuente de un proyecto tan simple).

Gradle: Un sistema muy groovy

Nos saltamos a 2008, cuando surge el proyecto Gradle (pronunciado más o menos greirol). Este sistema para construcción de proyectos toma lo mejor de lo que ya existe: puede integrar tareas de Ant, usar el manejo de dependencias de Ivy, ciclos de compilación y pruebas tipo Maven y, lo mejor de todo, sigue el paradigma de convención sobre configuración; es decir, todas las opciones configurables tienen valores por defecto con lo más común o útil, de modo que sólo es necesario modificarlos para casos especiales, pero para la mayoría de los casos se puede usar el valor por omisión, lo cual nos permite tener scripts bastante breves.

Una de las características que hacen a Gradle tan sencillo de usar, es que los scripts usan un lenguaje específico de dominio (Domain-Specific Language, DSL) que extiende el lenguaje de programación Groovy. Esto le da a Gradle a la vez sencillez y poder, ya que en los scripts se pueden utilizar elementos tanto de programación orientada a objetos como de programación funcional. Gradle cuenta también con una arquitectura de plugins y de entrada ofrece varios muy útiles: para compilar proyectos Java, proyectos Groovy (y por supuesto híbridos Java+Groovy), crear artefactos para publicar en repositorios Maven, generar documentación técnica (Javadoc/Groovydoc), realizar pruebas unitarias y generar reportes con los resultados, etc. Y por supuesto, cuenta con una API para que terceros puedan crear sus propios plugins.

Veamos un ejemplo sencillo, para lo cual necesitamos crear un proyecto muy sencillo, algo un poquito arriba del típico “Hola, Mundo!” para poder integrar pruebas unitarias y documentación.

Hola, Gradle

Lo primero, por supuesto, es instalar Gradle. El único prerrequisito previo es tener instalado un JDK. Teniendo esto, descargamos la versión más reciente de Gradle en http://gradle.org (que al momento de escribir este artículo, es 1.0-milestone-3), desempaquetamos el .zip en el destino de nuestra preferencia, y apuntamos a ese directorio la variable de ambiente GRADLE_HOME. También es recomendable agregar a la ruta de ejecutables (PATH) el directorio $GRADLE_HOME/bin para que podamos llamar al ejecutable de gradle desde cualquier directorio. Para probar que la instalación es correcta, podemos ejecutar en una línea de comando gradle -v y nos debe regresar el número de versión de gradle que instalamos.

Una vez que hemos completado la instalación, crearemos un proyecto Java. Por el momento, vamos a crear de forma manual la estructura de directorios del proyecto, pueden hacerlo ya sea desde línea de comando o con el administrador de archivos de su sistema operativo. Hay que crear un directorio raíz para el proyecto, HolaSG (Gradle usará el nombre de este directorio para el nombre del proyecto) y debemos tener la siguiente estructura de directorios debajo:

src/main/java/ejemplo
src/main/resources
src/test/java/ejemplo
src/test/resources

Si les parece conocida la estructura, es porque está basada en la que Maven ha hecho tan popular. En src/main/java va el código fuente, src/test/java contiene las pruebas unitarias. Los directorios src/main/resources y src/test/resources son para recursos auxiliares (XML, properties, imágenes, etc), tanto para el proyecto como para sus pruebas unitarias, pero este ejemplo es muy simple y no vamos a usar archivos auxiliares, de modo que podemos simplemente omitir estos directorios.

El código

Ahora sí, vamos a crear una clase en Java:

Por ahora vamos a saltarnos la prueba unitaria, para irnos directo a compilar. A fin de cuentas, este artículo es de Gradle, no de Test Driven Development.

El script

El script de Gradle debe ir directamente bajo el directorio HolaSG.

Puede tener cualquier nombre, pero así como en ant la convención es un build.xml, en Gradle es un build.gradle. Y debe contener la fabulosa cantidad de una línea de código:

Como mencioné al principio, Gradle usa la convención sobre la configuración. De modo que lo único que estamos haciendo aquí es indicarle que cargue el plugin para proyectos Java, y ese plugin se encargará del resto. Este plugin es bastante completo, ya que va a descargar las dependencias necesarias (ninguna por el momento), compilar el código, ejecutar las pruebas unitarias y hasta puede generarnos un JAR con las clases compiladas. Vamos a ejecutarlo, en la línea de comando debemos ir al directorio HolaSG y teclear:

gradle build

Veremos cómo se ejecutan varias tareas: compileJava (compila el código), processResources (copia los archivos de src/main/resources a donde quedaron las clases compiladas), classes, jar, assemble (arma los artefactos o entregables definidos para el proyecto), compileTestJava (compila las pruebas unitarias), processTestResources (copia los archivos de src/test/resources a donde quedaron las pruebas unitarias compiladas), testClasses y test (que ejecutan las pruebas unitarias), check y finalmente, build. Debemos al final ver el mensaje de BUILD SUCCESSFUL y el tiempo que le tomó todo el proceso.

Ahora podemos ver que tenemos dos directorios nuevos bajo HolaSG: .gradle y build. El .gradle es un directorio interno de Gradle para almacenar el estado de todos los recursos involucrados en la construcción del proyecto, lo cual le permite saber entre otras cosas, qué clases hay que recompilar en ejecuciones subsecuentes. Y el directorio build contiene el resultado del proceso. Dentro podemos ver un directorio classes/main y ahí encontraremos nuestro HolaJava.class; y si vemos en libs encontraremos un HolaSG.jar.

Si ejecutamos nuevamente gradle build, veremos la lista de tareas nuevamente pero junto a cada una saldrá la leyenda UP-TO-DATE, esto gracias al cache que le permite a Gradle saber que todo está actualizado y por lo tanto ninguna tarea hizo realmente nada.

Pruebas unitarias

Bien, ahora que ya vimos a Gradle en acción por primera vez, agreguemos una prueba unitaria al proyecto. En este caso utilizaré jUnit para mis pruebas, para mostrar lo fácil que es configurarlo y utilizarlo:

Y ahora debemos agregar unas líneas al script de Gradle, para que quede así:

Con esto modificamos la configuración de Gradle: primero, definimos repositorios de código para poder descargar las dependencias necesarias. Dado que el repositorio central de Maven es el lugar más utilizado, Gradle ya tiene un método para agregar su configuración a los repositorios, de modo que sólo tenemos que invocarlo.

Y para las dependencias, estamos indicando que para la compilación de las pruebas unitarias queremos usar jUnit 4.8.2. Las dependencias se pueden indicar simplemente con una cadena en formato groupID:artifactID:version para buscarlas en el repositorio de Maven. En este caso definimos dependencias para la tarea de compilación de pruebas con testCompile, pero también lo podemos hacer para otras tareas como compile (compilación de clases del proyecto), runtime (ejecución de la aplicación) o testRuntime (se usan para correr las pruebas, no para compilarlas). Es posible definir dependencias para tareas adicionales en caso que fuera necesario, pero estas que menciono son las más comunes.

Para ejecutar las pruebas tecleamos gradle test. Veremos pasar las tareas de compileJava y demás con la señal UP-TO-DATE y posteriormente compileTestJava donde se compila nuestra nueva clase de prueba. Luego, al llegar a la tarea test que es donde se ejecutan las pruebas, nos encontraremos con un mensaje de error. La prueba unitaria falló (la clase HolaJava tiene un error intencional). Podemos abrir el reporte en build/reports/tests/index.html para ver la razón (Gradle nos entrega un reporte muy bonito en HTML con el resultado de las pruebas unitarias, ver figura 1).

Al revisar la prueba podemos ver que tenemos un defecto en nuestra clase HolaJava. Corregimos el código, cambiando la línea que genera el saludo para agregar el parámetro faltante:

return String.format(“Hola, %s! (en Java)”, quien);

Ahora ejecutamos nuevamente las pruebas con gradle test para verificar que salen bien.

Aprovechemos este momento para agregarle una etiqueta con la versión y descripción a nuestro proyecto. Para agregar estos datos, simplemente hay que agregar unas líneas en nuestro script de gradle, debajo de nuestra línea donde invocamos el plugin de java.

version = ‘0.1’ description = ‘Ejemplo de uso de Gradle para Revista SG’


Documentación Técnica

La documentación técnica es una parte fundamental de todo proyecto. En Java tenemos la facilidad del javadoc que nos permite generar documentación de cada clase y cada método a partir de los comentarios que se le pongan al código, siguiendo ciertos lineamientos. El problema generalmente es generar la documentación, ya que el comando es muy engorroso de invocar. Herramientas como Ant lo hacen un poco menos complicado y Maven incluye lo necesario para poder generar la documentación; por supuesto Gradle no se queda atrás.

Primero que nada, hay que agregar los comentarios a nuestro código, para que haya algo que generar:

Y ahora simplemente debemos ejecutar gradle javadoc y después podemos abrir en un navegador el archivo build/docs/javadoc/index.html para ver la documentación generada.

Adicionalmente, en proyectos de tipo biblioteca de clases, es común generar por separado un JAR con el javadoc y otro JAR con los archivos de código fuente (en proyectos de software libre y en proyectos subcontratados), para tener una distribución completa.

Para crear los JARs, necesitamos definir dos tareas nuevas. En Gradle, las tareas llevan un nombre, un tipo, y algunas otras propiedades, como dependencias con otras tareas, etc. Cada tarea puede tener propiedades distintas. Una tarea se declara con la palabra task y el nombre de la misma. Cada tarea lleva una serie de acciones y se le pueden agregar acciones a las tareas existentes, así como modificar sus propiedades. Gradle mantiene una lista de todas las tareas en la propiedad tasks y se pueden obtener por nombre.

Primero agreguemos a las opciones de javadoc la lista de ligas externas con la referencia a la documentación de Java, para que el parámetro y valor de retorno del método saluda tengan referencia a la clase String. Agreguemos esta linea al final de build.gradle:

tasks.javadoc.options.links=[
‘http://download.oracle.com/javase/6/docs/api/’
]

En esa lista se pueden agregar referencias a la documentación de otras dependencias del proyecto, pero por el momento no tenemos ninguna. Las tareas que necesitamos definir para crear los JARs son estas (las podemos agregar al final del script):

task javadocJar(type:Jar, dependsOn:’javadoc’) {
from javadoc.destinationDir
classifier=’javadoc’
}
task sourcesJar(type:Jar) {
from sourceSets.main.allSource
classifier=’sources’
}

Lo que estamos haciendo es definir la tarea javadocJar, de tipo Jar y que depende de la tarea javadoc; es importante definir esta dependencia para obligar a que primero se genere la documentación, sino vamos a generar un jar vacío o con documentación desactualizada. Para el segundo Jar no tenemos dependencias porque sólo vamos a incluir los fuentes, pero hay que indicar que queremos únicamente los fuentes del proyecto y no los de las pruebas. Cabe mencionar que el plugin de Java agrega el concepto de sourceSets a Gradle, que son precisamente los conjuntos de archivos de código fuente. Por omisión se definen dos conjuntos: main, con las clases del proyecto, y test, con las pruebas unitarias. Por omisión, los fuentes del conjunto main están en src/main/java. Dado que estamos siguiendo convenciones, no es necesario que especifiquemos todo esto de forma explícita en nuestro script. En la tarea de sourcesJar, estamos incluyendo TODOS los fuentes (incluyendo recursos) del conjunto main.

Y a cada Jar le pusimos un clasificador, el cual irá en el nombre del archivo resultante, ya que por omisión Gradle va a crear los Jars usando el nombre del proyecto, la versión y el clasificador.

Y ahora podemos ejecutar estas tareas de manera secuencial, simplemente hay que indicar cada una en la línea de comando. De modo que tecleamos gradle javadocJar sourcesJar y al final en el directorio build/libs tendremos HolaSG-0.1-javadoc.jar y HolaSG-0.1-sources.jar.

Pero estas son tareas definidas específicamente en este proyecto, no parecen ser algo estándar; la idea de estos scripts y herramientas es facilitarle la vida a cualquier persona que quiera construir el proyecto, permitiéndoles ejecutar el script sin tener siquiera que verlo; honestamente, ¿cuántos de nosotros hemos visto un Makefile de un proyecto que bajamos y construimos desde fuentes? Simplemente le damos ./configure && make && sudo make install y listo. Afortunadamente, el plugin de Java para Gradle detecta cualquier tarea de tipo Jar y la agrega automáticamente a la tarea de assemble. Y esa tarea a su vez depende de las de compilación (pero no de las de pruebas). De modo que podemos definir que algunas tareas se ejecuten por omisión en nuestro script. Agreguemos esta línea después de donde definimos la versión:

defaultTasks ‘build’, ‘assemble’

Ahora, podremos simplemente ejecutar gradle sin ningún argumento adicional y se ejecutarán las tareas de compilación, pruebas, javadoc, etc. Puedes borrar todo tu directorio build antes de correr la tarea, para asegurar que sí se recree todo. Al final, en build/libs tendremos tres JARs: el binario, el de fuentes y el de documentación.

Capacidades avanzadas

La simplicidad de uso de Gradle puede ser engañosa, pues es realmente una herramienta muy, muy poderosa. En esta ocasión quise resaltar lo sencillo que puede ser su uso, para compilar un proyecto muy sencillo. Algunas capacidades avanzadas que no se cubrieron en este tutorial de introducción pero que seguramente requerirán en proyectos complejos son: incluir dependencias en la configuración de compile y tal vez la de testRuntime; agregar dependencias locales, para esos casos tan comunes en que se tiene un directorio con muchos JARs existentes; convertir el proyecto en uno políglota, cambiando el plugin de Java por el de Groovy para compilar y probar clases en ambos lenguajes; crear scripts multi-proyecto, para poder compilar, armar y juntar varios proyectos con un solo comando, manejando dependencias y otras configuraciones tanto de manera individual como global. Esta última capacidad hace a Gradle mucho muy superior a otras herramientas de construcción populares. También es posible crear tareas de manera dinámica, es decir, que no están definidas de manera formal en el script pero éste contiene código que al momento de ejecutarlo, genera tareas que pueden depender de otras tareas y agregarse a las existentes. Esto parece muy esotérico al principio, pero realmente puede facilitarnos la vida, especialmente cuando se usa Gradle para organizar, integrar y automatizar la construcción de proyectos existentes.

El código fuente utilizado para este tutorial está disponible en: https://github.com/chochos/HolaSG

Bio

Enrique Zamudio es Licenciado en Sistemas Computacionales egresado de la Universidad Iberoamericana y tiene 17 años desarrollando software profesionalmente, principalmente utilizando tecnologías relacionadas con Java del lado del servidor. Es autor de los proyectos de software libre jAlarms y j8583, y forma parte del staff de la comunidad de desarrolladores JavaMéxico. @chochosmx github.com/chochos.