miércoles, 25 de marzo de 2015

Gradle - Introducción (Tutorial Básico)

¿Qué es Gradle?

Gradle es una herramienta para manejar y automatizar los procesos de compilación, construcción e inclusive despliegue de proyectos de software. Aunque inicialmente fue concebido para funcionar con proyectos Java y lenguajes de la máquina virtual de Java (Groovy, Scala) actualmente soporta plataformas y lenguajes adicionales como C/C++, JavaScript, etc. Actualmente ha sido la herramienta escogida para la gestión del código fuente de Android.

Gradle se desarrolló teniendo presente las ventajas que han traído las anteriores herramientas de construcción como el completo control del proceso de Apache Ant, la administración de dependencias de Ivy y la extensiblidad de Apache Maven, a la vez que se pretendía ofrecer ventajas sobre dichas herramientas como:
  • Scripts de construcción más fáciles de entender: Gradle usa un DSL basado en Groovy para el archivo de configuración del proyecto, el cual es más corto y más fácil de entender y mantener que los archivos XML tradicionales
Imagen tomada de http://www.drdobbs.com/jvm/why-build-your-java-projects-with-gradle/240168608
  • Un proceso de construcción más claro: basado en tareas secuenciales, ordenadas según las dependencias de cada tarea entre sí, en forma de grafo dirigido sin ciclos, para garantizar que se ejecutan en un orden correcto sin repetirse, pudiendo empezar desde cualquiera de las tareas (nodos) y Gradle sabe cual(es) debe ejecutar a partir de allí.

Imagen tomada de: http://en.wikipedia.org/wiki/Directed_acyclic_graph
  • Realiza una compilación y construcción parcial: en lugar de realizar siempre la compilación y construcción de todo el proyecto, Gradle usa construcciones incrementales (incremental builds), con lo cual solamente construye los módulos del proyecto que fueron modificados, haciendo que probar pequeños cambios en proyectos muy grandes sea más fácil y eficiente.
Existen más razones, pero para no hacer el post más largo de lo necesario se puede leer el siguiente artículo, en el cual se explican muy bien: http://www.drdobbs.com/jvm/why-build-your-java-projects-with-gradle/240168608

Ejemplo desde la Línea de Comandos (Terminal)

Para entender mejor cómo funciona Gradle y cómo se puede usar en proyectos Java, es mejor empezar con un ejemplo básico desde la línea de comandos, en un siguiente post se mostrará como hacerlo desde el IDE Eclipse.

La aplicación de ejemplo se puede descargar desde: https://github.com/guillermo-varela/gradle-example/releases/tag/1.0.0

  1. Descargar Gradle desde https://www.gradle.org/downloads. Nota: solamente se requieren los binarios.
  2. Descomprimir el archivo descargado en la ruta de preferencia.
  3. Para mayor comodidad se recomienda agregar de la instalación de Gradle al "PATH" del sistema operativo: <ruta_gradle>/bin
  4. Abrir una instancia de la línea de comandos (Terminal) y comprobar que la instalación de Gradle funciona correctamente mediante el comando "gradle -v" el cual debe tener una respuesta como la siguiente:
    ------------------------------------------------------------
    Gradle 2.3
    ------------------------------------------------------------
    
    Build time:   2015-02-16 05:09:33 UTC
    Build number: none
    Revision:     586be72bf6e3df1ee7676d1f2a3afd9157341274
    
    Groovy:       2.3.9
    Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
    JVM:          1.8.0_40 (Oracle Corporation 25.40-b25)
    OS:           Linux 3.16.0-31-generic amd64
    
    
  5. Crear la siguiente estructura de carpetas, correspondientes al proyecto Java (se usa el estándar de carpetas de Maven, ya que es muy usado aún entre las personas que usan Gradle, a pesar de que puede usarse una estructura personalizada):
    gradle-example/
    ..src/
    ....main/
    ......java/
    ........example/
    
  6. Dentro de la carpeta "example" crear el archivo "Example.java" con el siguiente contenido:

    package example;
    
    public class Example {
      public static void main(String[] args) {
        System.out.println("This is an example");
      }
    }
     
  7. Hasta ahora se tiene un proyecto muy simple de Java (solamente imprime una cadena). Ahora se debe crear el archivo de configuración de gradle llamado "build.gradle" en la raíz del proyecto (carpeta "gradle-example"):
    apply plugin: 'java'
    apply plugin: 'application'
    
    version = '1.0.0'
    sourceCompatibility = 1.6
    targetCompatibility = 1.6
    
    mainClassName = 'example.Example'
    
    jar {
        manifest {
            attributes 'Implementation-Title': 'Gradle Example', 'Implementation-Version': version
            attributes 'Main-Class': mainClassName
        }
    }
    
    Con la configuración anterior ya se le está indicando a Gradle que:
    • Debe construir el proyecto como un proyecto Java.
    • Se aplica el plugin "application", el cual habilita varias opciones (comandos) que permiten ejecutar y construir el proyecto como una aplicación Java.
    • Se trata de la versión "1.0.0".
    • La versión de Java con la que se debe compilar el proyecto es "1.6".
    • La clase con el método "main" (punto de entrada de la aplicación) será "example.Example".
    • La aplicación se empaquetará en un archivo "JAR".
    • El archivo "META-INF/MANIFEST.MF" que estará dentro del JAR tendrá las propiedades del nombre de la aplicación, la versión y el nombre completo de la clase con el método "main". Cabe anotar que para la versión no se puso el valor "1.0.0" nuevamente, sino que se usó la variable (propiedad) "version" creada anteriormente en el archivo. Como se mencionaba anteriormente, Gradle usar un DSL, es decir el archivo de configuración en un programa. De igual manera se reutilizó la propiedad "mainClassName". A manera de ejercicio se deja la prueba de adicionar la instrucción "println 'Hello World'" al archivo "build.gradle", sólo para ver que pasa en la próxima construcción. Esta característica de los archivos de configuración Gradle permiten que el proceso de construcción sea dinámico y se tomen decisiones dependiendo de variables, ambientes, etc.

  8. Para ver los diferentes comandos y opciones que ahora tenemos disponibles para el proyecto con Gradle se puede ejecutar el comando "gradle tasks" desde la raíz del proyecto:
    :tasks
    
    ------------------------------------------------------------
    All tasks runnable from root project
    ------------------------------------------------------------
    
    Application tasks
    -----------------
    installApp - Installs the project as a JVM application along with libs and OS specific scripts.
    run - Runs this project as a JVM application
    
    Build tasks
    -----------
    assemble - Assembles the outputs of this project.
    build - Assembles and tests this project.
    buildDependents - Assembles and tests this project and all projects that depend on it.
    buildNeeded - Assembles and tests this project and all projects it depends on.
    classes - Assembles classes 'main'.
    clean - Deletes the build directory.
    jar - Assembles a jar archive containing the main classes.
    testClasses - Assembles classes 'test'.
    
    Build Setup tasks
    -----------------
    init - Initializes a new Gradle build. [incubating]
    
    Distribution tasks
    ------------------
    assembleMainDist - Assembles the main distributions
    distTar - Bundles the project as a distribution.
    distZip - Bundles the project as a distribution.
    installDist - Installs the project as a distribution as-is.
    
    Documentation tasks
    -------------------
    javadoc - Generates Javadoc API documentation for the main source code.
    
    Help tasks
    ----------
    components - Displays the components produced by root project 'gradle-example'. [incubating]
    dependencies - Displays all dependencies declared in root project 'gradle-example'.
    dependencyInsight - Displays the insight into a specific dependency in root project 'gradle-example'.
    help - Displays a help message.
    projects - Displays the sub-projects of root project 'gradle-example'.
    properties - Displays the properties of root project 'gradle-example'.
    tasks - Displays the tasks runnable from root project 'gradle-example'.
    
    Verification tasks
    ------------------
    check - Runs all checks.
    test - Runs the unit tests.
    
    Other tasks
    -----------
    wrapper
    
    Rules
    -----
    Pattern: clean: Cleans the output files of a task.
    Pattern: build: Assembles the artifacts of a configuration.
    Pattern: upload: Assembles and uploads the artifacts belonging to a configuration.
    
    To see all tasks and more detail, run gradle tasks --all
    
    To see more detail about a task, run gradle help --task 
    
    BUILD SUCCESSFUL
    
    Total time: 3.859 secs
    
  9. Según se pudo ver en el punto anterior, ya se tiene disponible la tarea "run", la cual permitirá ejecutar el proyecto como una aplicación Java, mediante el comando "gradle run":
    :compileJava UP-TO-DATE
    :processResources UP-TO-DATE
    :classes
    :run
    This is an example
    
    BUILD SUCCESSFUL
    
    Total time: 3.583 secs
    
    Como puede verse, la tarea "run" sólo se ejecutó hasta el final, cuando Gradle ejecutó las tareas de las que se dependía, y como resultado se ejecutó el código de la aplicación (imprimió la cadena "This is an example").

  10. Adicionalmente se tiene disponible también la tarea "build", la cual construirá el proyecto y ejecutará las pruebas unitarias (en caso de tenerlas), mediante el siguiente comando "gradle build":
    :compileJava UP-TO-DATE
    :processResources UP-TO-DATE
    :classes UP-TO-DATE
    :jar
    :startScripts
    :distTar
    :distZip
    :assemble
    :compileTestJava UP-TO-DATE
    :processTestResources UP-TO-DATE
    :testClasses UP-TO-DATE
    :test UP-TO-DATE
    :check UP-TO-DATE
    :build
  11. En este punto Gradle debe haber creado la siguiente estructura de carpetas adicionales:
    gradle-example/
    ..".gradle/"
    ..build/
    ....classes/
    ....dependency-cache/
    ....distributions/
    ......gradle-example-1.0.0.tar
    ......gradle-example-1.0.0.zip
    ....libs/
    ......gradle-example-1.0.0.jar
    ....scripts/
    ......gradle-example
    ......gradle-example.bat
    ....tmp/
    
    Realmente lo destacable en este punto son las carpetas:

    • "libs" en la cual se almacena el JAR generado para la aplicación "gradle-example-1.0.0".
    • "distributions" en la cual se almacenan los archivos (TAR y ZIP como opciones) para distribuir la aplicación. La diferencia con el JAR generado en la carpeta "llibs" es que estos archivos comprimidos contienen el JAR de la aplicación y también las dependencias y demás archivos necesarios. Esto quedará un poco más claro cuando se agregue una dependencia más adelante.
    • "scripts" en la cual se almacenan scripts en Bash (para sistemas basados en Unix) y Batch (para sistemas Windows), los cuales permiten ejecutar la aplicación solamente con ejecutar el script. Los parámetros necesarios (como el classpath) ya se agregan en cada uno de los scripts. Estos archivos también se encuentran dentro de los archivos comprimidos en "distributions".

  12. La aplicación ahora se puede ejecutar de dos maneras:
    1. Directamente desde el JAR:
      java -jar build/libs/gradle-example-1.0.0.jar
      This is an example
      
    2. Descomprimiendo alguno de los archivos comprimidos en "distributions" y ejecutando el script correspondiente al sistema operativo usado:
      build/distributions/gradle-example-1.0.0/bin/gradle-example
      This is an example
      

Gradle Wrapper

Esta es una opción que facilita la construcción con Gradle, tanto el código compartido entre un equipo de trabajo como en servidores de entrega continua. Básicamente es un conjunto de scripts (batch y shell), los cuales permiten ejecutar la construcción con Gradle, a pesar de que el computador actual no tenga instalado Gradle. Para usarlo se debe agregar la siguiente entrada al archivo "build.gradle":
task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}
Este fragmento está creando la tarea de Gradle "wrapper" y se está indicando que la versión de Gradle a usar es "2.3". Esta tarea se ejecuta como cualquier otra tarea de Gradle: "gradle wrapper".

Una vez ejecutada la tarea, se crea la siguiente estructura:

gradle-example/
..gradle/
....wrapper/
......gradle-wrapper.jar
......gradle-wrapper.properties
..gradlew
..gradlew.bat

Los archivos "gradlew" y "gradle.bat" son los scripts shell y batch que se mencionaron previamente y se encargan de ejecutar el archivo "gradle-wrapper.jar", el cual a su vez descarga, descomprime y ejecuta la versión de Gradle que se indicó en la tarea, y que queda también en el archivo "gradle-wrapper.properties", de esta manera también se garantiza que todas las construcciones se van a realizar con la misma versión de Gradle. Una vez generados estos archivos, no es necesario volver a ejecutar la tarea "wrapper".

Todos estos archivos deben ser almacenados junto con el proyecto en el repositorio de código, precisamente para que las demás personas del equipo o sistemas de construcción automatizado usen los mismos archivos para la construcción.

Este wrapper permite ejecutar las mismas tareas que están disponibles para el comando "gradle", desde la raíz del proyecto, simplemente ejecutando el comando "gradlew". Por ejemplo, para ejecutar la construcción el comando sería "gradlew build" (aunque en sistemas Linux posiblemente se requiera usar "./gradlew build"):

gradlew build 
Downloading https://services.gradle.org/distributions/gradle-2.3-bin.zip
........................................................................
Unzipping /.../.gradle/wrapper/dists/gradle-2.3-bin/.../gradle-2.3-bin.zip to /.../.gradle/wrapper/dists/gradle-2.3-bin/...
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build UP-TO-DATE

BUILD SUCCESSFUL

Total time: 1 mins 29.447 secs 

Nota: En caso de querer actualizar la versión de Gradle que se usará mediante el Wrapper, solamente se requiere editar los valores correspondientes en el archivo "gradle-wrapper.properties", aunque si se desea usar alguna funcionalidad específica de una versión de Gradle, es posible que sea necesario volver a ejecutar la tarea "wrapper".

Dependencias

Gradle permite trabajar con dependencias tanto agregadas manualmente como archivos dentro del mismo proyecto (Flat directory repository), como con repositorios externos (públicos y privados) de Ivy y Maven.

Para mantener el ejemplo sencillo, se usará una funcionalidad simple de Apache Commons:
package example;
import org.apache.commons.lang3.StringUtils;

public class Example {
  public static void main(String[] args) {
    System.out.println("This is an example");
    System.out.println(StringUtils.swapCase("This is an example"));
  }
}
La librería se descargará desde el repositorio central de Maven, para lo cual se agregará lo siguiente al archivo "build.gradle":
repositories {
    mavenCentral()
}
dependencies {
    compile 'org.apache.commons:commons-lang3:3.3.2'
}

Dado que el repositorio central de Maven es un repositorio muy usado, Gradle ya tiene una función que permite indicarlo fácilmente, sin indicar URL. En este caso se agregó la librería Apache Commons como una dependencia necesaria para compilar el proyecto. Existen otras opciones (configuraciones) que permiten indicar dependencias sólo para pruebas (testCompile) entre otras.

De esta manera ya es posible volver a ejecutar el comando "gradlew build", lo cual generará de nuevo el JAR de la aplicación, sin embargo el JAR no tendrá la librería de Apache Commons, al igual que otros archivos JAR solamente contiene los archivos compilados de la propia aplicación. Las librerías externas deberán estar en una carpeta que se indique como parte del classpath:

java -cp "path_to_library/commons-lang3-3.3.2.jar:build/libs/gradle-example-1.0.0.jar" example.Example 

This is an example
tHIS IS AN EXAMPLE

Aquí es donde está la utilidad de los archivos comprimidos en "distributions", ya que estos, como se mencionó anteriormente, contienen las dependencias necesarias para ejecutar la aplicación de una manera más sencilla:
gradle-example-1.0.0.zip
..gradle-example-1.0.0
....bin
......gradle-example
......gradle-example.bat
....lib
......commons-lang3-3.3.2.jar
......gradle-example-1.0.0.jar

Basta con descomprimir alguno de los archivo y ejecutar:
build/distributions/gradle-example-1.0.0/bin/gradle-example
This is an example
tHIS IS AN EXAMPLE

JAR Auto-Contenido

En caso de que se quiera/necesite un solo archivo para ejecutar la aplicación, existe una manera de generar un JAR auto-contenido con todas las librerías necesarias para funcionar por su cuenta.

Para ello se creará una tarea adicional en "build.gradle" indicando que el JAR a generar debe contener las dependencias del proyecto (comúnmente llamado "fat JAR"). Aunque se puede agregar esta opción en la tarea "jar" se crea una nueva para dejar la posibilidad de generar el JAR sin las dependencias.
//create a single Jar with all dependencies
task fatJar(type: Jar) {
    baseName = project.name + '-all'
    manifest {
        attributes 'Implementation-Title': 'Gradle Example - All', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar
}
En este caso el nombre del JAR será el nombre del proyecto con el sufijo "-all" (sólo para demostrar una manera de modificar el nombre por defecto del archivo), se indicó con la instrucción "from" que debe usar las dependencias de compilación (configuración compile) y que debe incluir también los archivos compilados de este proyecto (con la instrucción "with jar").

gradlew fatJar
:compileJava
:processResources UP-TO-DATE
:classes
:fatJar

BUILD SUCCESSFUL

Total time: 5.05 secs

Ahora ejecutar la aplicación será mucho más fácil desde el JAR:
java -jar build/libs/gradle-example-all-1.0.0.jar
This is an example
tHIS IS AN EXAMPLE

Esto es básicamente Gradle. En general los IDEs tienen una integración más desarrollada, pero como introducción ayuda mucho conocer cómo funciona la herramienta, sin los atajos que muchas veces tienen dichos IDEs.

Referencias