[Número 001] Propiedades de Proyecto en Gradle

Resumen: Determinar el alcance de una propiedad de proyecto en Gradle puede ser confuso a veces. En esta primera edición examinaremos diferentes opciones para definir propiedades de proyecto y leer sus valores.

¡Bienvenidos a la primera edición de mi editorial! El objetivo de este editorial es compartir consejos y experiencias obtenidas a través de los años trabajando en proyectos comerciales y de código abierto. ¡Empecemos!

El ecosistema Java contiene una multitud impresionante de proyectos, en el ámbito de construcción de proyectos la mayoría de los programadores seguramente se ha topado con Apache Ant o Apache Maven de una manera u otra. Hace algunos años ya un nuevo proyecto apareció, Gradle, es probable que lo hayas encontrado un par de veces. Personalmente he usado Gradle desde sus inicios, debo confesar que la experiencia de verlo crecer y evolucionar ha sido gratificante y emocionante al mismo tiempo. Definiendo la herramienta de manera simplista, Gradle ofrece las mismas características que Ant y Maval ismo tiempo que ofrece mas beneficios. Una de las primeras decisiones tomadas por parte del equipo de desarrollo de Gradle fué el usar un Lenguage de Dominio Específico (DSL por sus siglas en Inglés) basado en un lenguage de programación concreto (Apache Groovy) en vez de seguir la pauta de sus predecesores y usar XML. Esta decisión resultó ser bastante atinada dado que resulta muy fácil extender el comportamiento de las instrucciones de construcción d un proyecto al escribir código ad-hoc. Sin embargo es posible encontrar elementos de configuración que resulten confusos, algunos de ellos relacionas con el uso del lenguage Groovy dada la naturaleza intrínseca de la ejecución de archivos tipo Script. Demos un vistazo a un problema común que he encontrado en diferentes equipos: el manejo de propiedades de proyecto.

Las propiedades del proyecto se utilizan para configurar y alterar el comportamiento de compilación de un proyecto. Sin entrar en detalles profundos, digamos que cada compilación es una serie de operaciones aplicadas a una instancia de Proyecto. Este tipo define un pequeño conjunto de propiedades estáticas tales como group y version, comúnmente usadas para definir coordenadas para su publicación en un repositorio de artefactos. Un proyecto también puede tener propiedades dinámicas que se pueden definir de 4 maneras:

  • en un archivo llamado gradle.properties, local al proyecto.
  • en un archivo llamado gradle.properties, disponible para todos los proyectos en ~/.gradle/(%USER_HOME%/.Gradle en Windows).
  • en la línea de comandos utilizando el indicador -P.
  • utilizando el bloque ext dentro del archivo build.gradle.

Vale la pena señalar que las propiedades estáticas y dinámicas pueden ser definidas por otros elementos de construcción como plugins, convenciones y extensiones, sin embargo, no cubriremos esos elementos en esta ocasión. Comenzemos con un archivo Gradle (normalmente llamado build.gradle) como el siguiente

build.gradle

println foo

Si intentas ejecutar el archivo en su estado actual obtendrás un error dado que la propiedad foo no ha sido definida. Podemos areglar el problema al definir un archivo llamado gradle.properties con el siguiente contenido

gradle.properties

foo = local

Ahora si, la invocación del archivo Gradle imprime la palabra "local" en la consola de comandos. Felicidades! Acabas de definir una propiedad dinámica en el proyecto utilizando una fuente externa. Personalmente, prefiero definir casi todas las propiedades dinámicas que proporcionan un valor literal estático de esta manera, ya que los archivos gradle.properties suelen ser pequeños y sólo pueden contener texto. La siguiente opción para definir una propiedad dinámica es utilizar el archivo gradle.properties disponible de manera global. Ten en cuenta que todas las propiedades definidas en este archivo están disponibles para todos los proyectos asociados con un usuario en particular. Además, las propiedades establecidas en este archivo se consideran privadas (específicas para el usuario), por lo que puedes utilizar este archivo para almacenar datos confidenciales como claves de APIs y contraseñas. Veamos pues como agregar una propiedad dinámica de manera global

~/.gradle/gradle.properties

foo = global

Invocando el archivo Gradle una vez mas imprime ahora la palabra "global" en la consola de comandos. Esto significa que las propiedades globales tienen prioridad sobre las locales. Es bueno saber esto ya que puedes definir valores predeterminados a nivel local pero sobreescribirlos en el global, por usuario. Recomiendo acordar con el equipo qué configuración se puede compartir entre todos los miembros y servidores remotos (como lo son los servidores de Integración Continua). Demos un paso más allá, es posible definir una variable sobre la marcha usando la línea de comandos, por ejemplo, ejecutando la misma compilación que antes invocando

$ gradle -Pfoo=command

Ahora la impresión resulta en "comand" en vez de "global". Esto nos indica que las propiedades definidas en la línea de comandos tienen mayor precedencia que las globales, lo cual nos permite ajustar el valor de propiedad para una sola ejecución sin cambiar las fuentes de compilación, genial! Pero aún no hemos terminado, hay una opción más que explorar. Edita el archivo de construcción de nuevo y haz que se vea como el siguiente

build.gradle

ext.foo = 'build'
println foo

La invocando del archivo Gradle ahora imprime la palabra "build" en la consola. ¿Qué sucede si intentáremos combinar esta última versión con un indicador de línea de comandos? Intentémoslo, al invocar comando anterior donde la propiedad foo se definió en el lugar con -P. ¿Conseguiste el resultado esperado? Aquí es donde algunas desarroladores se quedan contemplando la pantalla para averiguar que sucedió. La mitad de l,os desarrolladores esperaría que la invocación imprimiera la palabra "comand" mientras que otros adivinarían "build" como el resultado correcto. ¿Qué está pasando aqui? Parece que todas las definiciones externas se toman en cuenta primero seguidas del archivo Gradle. Tiene sentido dado que Gradle debe seguir un determinado orden de evaluación, siendo el script de compilación el último en ser ejecutado.

Y es aquí donde retomamos el tema de la selección de Groovy como lenguage de descripción de archivos Gradle. Miremos el siguiente archivo

build.gradle

def bar = 'value'
println bar

En este archivo no define una propiedad de proyecto, sino una variable de script. La diferencia radica en que la "propiedad" tiene una definición de tipo. Sí, def es un tipo de Groovy, denota el tipo dinámico. También puedes usar String, int, o cualquier otro tipo Java/Groovy. La distinción entre una propiedad de proyecto y una variable de script es muy importante, ya que puedes obtener resultados inesperados al leer el valor en diferentes contextos. Vamos a modificar el archivo Gradle una vez más

build.gradle

ext.foo = 'build'
def bar = 'value'
def cls_printFoo = { println foo }
def cls_printBar = { println bar }
def mtd_printFoo() { println foo }
def mtd_printBar() { println bar }

task printIt {
    doLast {
        println foo
        println bar
        cls_printFoo()
        cls_printBar()
        mtd_printFoo()
        mtd_printBar()
    }
}

Este archivo contiene una definición de propiedad de proyecto (foo) y una variable de script (bar). También define dos closures - en Groovy las closures son similares a las expresiones lambda de Java8 o las funciones de JavaScript, con algunas diferencias por supuesto. Estas closures imprimen la propiedad y la variable respectivamente. También hay un par de métodos que realizan la misma operación que las closures previamente definidas; observa el ligero cambio en la sintaxis utilizada para definirlos. Ahora, al ejecutar la archivo Gradle especificando la tarea printIt da como resultado una salida similar a

:printIt
build
value
build
value
build
:printIt FAILED

FAILURE: Build failed with an exception.

* Where:
Build file '/tmp/foo/build.gradle' line: 6

* What went wrong:
Execution failed for task ':printIt'.
> Could not get unknown property 'bar' for root project 'foo' of type org.gradle.api.Project.

Hmm, algo salió mal. Parece que el método mtd_printBar no puede leer el valor, el mensaje de excepción da una pista de lo que sucedió. Parece que el método intentó leer la propiedad del proyecto llamada bar pero sabemos que en realidad es una variable de script. Para entender lo que está sucediendo uno tiene que estar consciente de las diferencias entre closures y métodos en Groovy, más específicamente cómo funciona la delegación en closures. Cada closure en Groovy tiene una propiedad llamada delegate, que puede usarse para alterar el comportamiento del closure a través de múltiples invocaciones. De forma predeterminada, un delegate define su valor inicial para que coincida con otra propiedad de sólo lectura del closure, el owner. Esta es la instancia que posee al closure, en nuestro caso es el propio archivo Gradle. A continuación, al ejecutar el archivo Gradle se actualizan las propiedades delegate para que coincidan con la instancia de Project. Cuando se ejecuta la closure cls_printFoo ésta intenta resolver la referencia a foo, y lo encuentra en su delegate, el Proyecto, por lo que tiene éxito. Cuando se ejecuta la closure cls_printBar, intenta resolver la referencia a bar, al no encontrarla en las propiedades del Proyecto, pero la encuentra como una variable de script puesto que la closure tiene acceso al mismo. Ahora, cuando se llama al mtd_printFoo, se invoca realmente en el propio proyecto, por lo que la referencia a foo se encuentra correctamente. Pero ese no es el caso cuando se invoca el último método. Resulta entonces que un método definido en un script se define de igual manera que cualquier otro método definido en cualquier otro lugar, es decir, los métodos no entienden el concepto de owner y delegate, por lo que no puede alcanzar a las variables del script.

Con esto concluímos la primera edición de esta editorial. Gracias por tu tiempo. Cualquier comentario es apreciado.

Nos vemos la próxima vez.

Andrés

ˆ Back To Top