Skip to main content

Convention plugins

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/echo-app-kt

En proyectos con múltiples módulos, es común y recomendable contar con un módulo dedicado a centralizar configuraciones compartidas que puedan ser reutilizadas por el resto del proyecto. En el contexto de Gradle, a este tipo de módulo se le conoce como un convention plugin, y también se asocia con el patrón conocido como buildSrc.

Estos plugins permiten consolidar configuraciones comunes, lo que mejora la consistencia entre módulos y simplifica el mantenimiento general del proyecto.

Más adelante profundizaremos en el uso de plugins personalizados dentro del ecosistema Gradle. Por ahora, comenzaremos con una configuración básica.

🧩 Configuración de un Convention Plugin

Lo primero que debemos hacer es definir un módulo que se cargue antes que los demás. Esto se configura en el archivo settings.gradle.kts, asegurando que el módulo de plugins esté disponible para el resto del proyecto desde el inicio.

settings.gradle.kts
// ...
pluginManagement {
includeBuild("convention-plugins")
repositories {
mavenCentral()
gradlePluginPortal()
}
}
¿Qué acabamos de hacer?
  • [2–8]: Esta sección configura la gestión de plugins en Gradle. Aquí es posible personalizar cómo se resuelven y desde dónde se descargan.
  • [3]: Se incluye el módulo convention-plugins como un build externo (includeBuild). Este módulo contendrá las configuraciones comunes que serán aplicadas automáticamente en el resto del proyecto. Al estar declarado aquí, se garantiza que se cargue antes que los demás módulos.
  • [4–7]: Se definen los repositorios desde los cuales se obtendrán los plugins:
    • [5]: mavenCentral(): Repositorio central de Maven, ampliamente utilizado para dependencias y plugins.
    • [6]: gradlePluginPortal(): Repositorio oficial de Gradle para plugins.

🗂️ Configuración de Convention Plugins

El siguiente paso es crear la estructura de directorios necesaria para definir nuestros convention plugins. Estos plugins encapsulan configuraciones comunes que podrán ser reutilizadas en otros módulos del proyecto.

En este ejemplo, omitiremos el directorio de pruebas, ya que este tipo de módulo no suele requerir código testeable: su propósito es únicamente configurar comportamientos comunes.

🏗️ Crear la Estructura de Directorios

Primero, crea el directorio donde ubicaremos el código fuente del plugin de convenciones:

New-Item -Path "convention-plugins\src\main\kotlin" `
-ItemType "Directory" `
-Force
¿Qué acabamos de hacer?

Este comando crea el directorio convention-plugins\src\main\kotlin, incluyendo todas las carpetas intermedias si no existen. El parámetro -ItemType "Directory" indica que se debe crear un directorio, y -Force asegura que no falle si alguna parte del path ya existe. La barra invertida invertida (`) al final de cada línea permite dividir el comando en varias líneas para mayor legibilidad.

⚙️ Configuración de settings.gradle.kts

Dentro de la carpeta principal convention-plugins, debemos crear el archivo settings.gradle.kts. Este archivo configura el nombre del proyecto, la gestión de plugins y la resolución de dependencias para este módulo.

convention-plugins/settings.gradle.kts
rootProject.name = "convention-plugins"

// Gestión de plugins y definición de repositorios para su descarga
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}

// Configuración para la resolución de dependencias
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}

// Definición de un catálogo centralizado de versiones de dependencias
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
¿Qué acabamos de hacer?

Este archivo settings.gradle.kts define la configuración principal del módulo convention-plugins. En la línea 1 se establece el nombre del proyecto, necesario para que Gradle lo identifique correctamente como un build independiente.

La sección pluginManagement (líneas 4–9) indica desde qué repositorios deben descargarse los plugins utilizados en este módulo. Aquí se usan mavenCentral() y gradlePluginPortal(), que cubren la mayoría de los plugins comunes en proyectos Java y Kotlin.

La sección dependencyResolutionManagement (líneas 12–25) configura cómo se resolverán las dependencias declaradas en los archivos build.gradle.kts del módulo. Se especifican los mismos repositorios, y además se declara un catálogo de versiones (versionCatalogs) en la línea 20, apuntando al archivo libs.versions.toml ubicado fuera del módulo. Este catálogo permite mantener versiones centralizadas y consistentes en todos los módulos del proyecto.

@Suppress("UnstableApiUsage")

Se incluye la anotación @Suppress("UnstableApiUsage") porque la propiedad repositories dentro de dependencyResolutionManagement se considera inestable en las versiones actuales de Gradle, y su uso genera un warning en compilaciones con Kotlin DSL.

¿Por qué aparece el warning sobre "UnstableApiUsage"?

En el archivo settings.gradle.kts del módulo convention-plugins, se utiliza la anotación @Suppress("UnstableApiUsage") antes de la configuración de dependencyResolutionManagement. Esto se debe a que, en las versiones actuales de Gradle, al invocar repositories dentro de dependencyResolutionManagement, se genera una advertencia indicando que la función está marcada como inestable con la anotación @Incubating.

La anotación @Incubating en Gradle señala que una característica o API está en fase de incubación, lo que significa que aún está en desarrollo y podría sufrir cambios en futuras versiones. Aunque estas funcionalidades son utilizables, es importante ser consciente de su estado y de la posibilidad de modificaciones. Para suprimir las advertencias relacionadas con el uso de APIs inestables, se emplea @Suppress("UnstableApiUsage"), lo que permite mantener el código limpio y sin advertencias innecesarias.

Es recomendable revisar periódicamente la documentación oficial de Gradle para estar al tanto del estado de las APIs utilizadas y ajustar el código en consecuencia a medida que evolucionan hacia versiones estables.

📑 Catálogo de Versiones

Aunque no es obligatorio, es una buena práctica centralizar las versiones de las dependencias de un proyecto utilizando un archivo .toml. Este enfoque garantiza consistencia en las versiones utilizadas en todo el proyecto, evitando problemas de incompatibilidad y facilitando el mantenimiento.

El catálogo de versiones se organiza en cuatro secciones principales:

  • Versions: Especifica las versiones de las dependencias, permitiendo su reutilización en las secciones de bibliotecas y plugins.
  • Libraries: Define las bibliotecas necesarias para el proyecto, asociándolas con las versiones especificadas en la sección de versiones.
  • Bundles: Agrupa varias bibliotecas bajo un mismo nombre, facilitando la inclusión de conjuntos de dependencias relacionadas.
  • Plugins: Configura los plugins del proyecto, también vinculados a las versiones centralizadas.

Este enfoque te ayuda a tener un control centralizado sobre las versiones, evitando la repetición de versiones en múltiples archivos y garantizando que todas las dependencias y componentes utilicen las mismas versiones.

Ejemplo de archivo libs.versions.toml:

Definiremos un catálogo de versiones en un archivo libs.versions.toml para centralizar las versiones de las dependencias de nuestro proyecto:

gradle/libs.versions.toml
[versions]
kotlin = "2.1.10"
echo = "1.0.0" # Versión de nuestro proyecto

[libraries]
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
¿Qué acabamos de hacer?
  • La sección [versions] define las versiones que serán utilizadas en las dependencias. En este caso, se especifica la versión de Kotlin y una versión para nuestro proyecto (echo).
  • En la sección [libraries], definimos la biblioteca kotlin-gradle-plugin y vinculamos su versión a la referencia kotlin especificada anteriormente en la sección [versions].
Nombre libs.versions.toml

El nombre libs.versions.toml es solo una convención y puedes elegir cualquier nombre que refleje su propósito, incluso dividirlo en varios archivos si es necesario. En este curso utilizaremos esta convención de nombre, ya que es común en la comunidad de Gradle.

Al usar esta estructura, cualquier actualización futura de la versión de Kotlin solo requiere modificar el archivo libs.versions.toml en un único lugar, manteniendo la consistencia en todo el proyecto.

🛠️ Crear el archivo de construcción

Dentro de la carpeta convention-plugins, crea un archivo de construcción llamado build.gradle.kts. Este archivo define cómo se compila y configura el módulo de plugins de convención.

convention-plugins/build.gradle.kts
plugins {
`kotlin-dsl`
}

repositories {
mavenCentral()
gradlePluginPortal()
}

dependencies {
implementation(libs.kotlin.gradle.plugin)
}
¿Qué acabamos de hacer?

Para que los plugins de convención escritos con Kotlin DSL sean detectados correctamente por Gradle, es necesario aplicar el plugin kotlin-dsl.

  • [2] `kotlin-dsl`: Se aplica el plugin usando backticks debido a que el nombre contiene un guion, lo que lo hace inválido como identificador normal en Kotlin. Este plugin habilita la escritura de scripts en Kotlin y permite que los plugins precompilados sean descubiertos automáticamente por el sistema de compilación.
  • [5–8] repositories: Especifica de dónde se obtendrán las dependencias y plugins necesarios. Se utilizan mavenCentral() y gradlePluginPortal() por su amplio soporte.
  • [11] implementation(libs.kotlin.gradle.plugin): Agrega el plugin de Kotlin para Gradle como dependencia. Esta línea asume que el proyecto utiliza un catálogo de versiones (libs.versions.toml), lo cual permite gestionar versiones de forma centralizada y coherente.

Backticks en el nombramiento de variables y funciones

En Kotlin, los backticks (`...`) permiten declarar identificadores que normalmente no serían válidos, como aquellos que contienen guiones (-) o espacios.

Esto es especialmente útil en archivos build.gradle.kts, donde algunos plugins tienen nombres con guiones. Por ejemplo:

plugins {
`kotlin-dsl` // válido gracias a los backticks
}

Sin los backticks, kotlin-dsl no sería un identificador válido porque contiene un guion (-).

¿Por qué se usan?

Este mecanismo existe principalmente por compatibilidad con Groovy, el lenguaje original usado por Gradle. En Groovy, los nombres de plugins como kotlin-dsl se declaraban como cadenas:

plugins {
id "kotlin-dsl"
}

Para mantener una sintaxis similar en Kotlin DSL, se permite el uso de backticks alrededor del nombre. Así, la migración y la legibilidad entre ambos DSLs es más natural.

Buenas prácticas
  • Úsalos solo cuando sea necesario: Los backticks deben reservarse para nombres que no serían válidos como identificadores normales.
  • Evita abusar de nombres arbitrarios: Aunque Kotlin permite cualquier texto entre backticks, como `some weird name`, eso puede volver tu código menos mantenible.
  • Prefiere nombres válidos sin backticks si es posible, para aprovechar el soporte completo de autocompletado y verificación estática.

🧪 Mi Primer Plugin

Plugin


Un plugin es un componente de software que extiende o modifica la funcionalidad de un sistema existente.
En el contexto de Gradle, un plugin es un conjunto de tareas, configuraciones y comportamientos que se pueden aplicar a un proyecto para automatizar su construcción y administración.

Gradle permite definir plugins de distintas formas. Una de las más utilizadas es mediante plugins de convención, que encapsulan configuraciones comunes aplicadas automáticamente a todos los subproyectos de un proyecto.

En esta sección, crearemos un plugin de convención básico para proyectos basados en la JVM. Comenzaremos con una configuración mínima y la iremos expandiendo en las siguientes secciones.

✨ Creando el Plugin

Primero, vamos a crear el archivo que contendrá nuestro plugin. Dentro de la carpeta convention-plugins/src/main/kotlin, crea un archivo llamado jvm.conventions.gradle.kts con el siguiente contenido:

convention-plugins/src/main/kotlin/jvm.conventions.gradle.kts
plugins {
kotlin("jvm")
}

kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
¿Qué acabamos de hacer?

Este archivo define un plugin de convención que aplica automáticamente el plugin kotlin("jvm") y configura la versión del compilador de Java a través del bloque jvmToolchain.

  • [2]: Aplica el plugin de Kotlin para la JVM. Al estar definido en un archivo de convención, se aplicará de forma implícita a todos los módulos que utilicen este plugin.
  • [5–9]: Configura la herramienta de compilación (jvmToolchain) para usar Java 21 como versión del lenguaje. Esto garantiza que todos los proyectos que usen este plugin compilen con la misma versión de Java, asegurando compatibilidad y consistencia.

❓ ¿Por qué no especificamos la versión del plugin?

Aunque es común declarar el plugin con una versión explícita, como en el siguiente ejemplo:

plugins {
kotlin("jvm") version "2.1.20"
}

en este caso no es necesario, ya que hemos centralizado la gestión de versiones mediante el archivo libs.versions.toml. Esta estrategia asegura que todas las dependencias y plugins de Kotlin utilicen la misma versión, facilitando su actualización y manteniendo coherencia en todo el proyecto.

📦 Usando el Plugin

Una vez creado nuestro plugin de convenciones, podemos aplicarlo en los subproyectos donde queramos que se utilicen esas configuraciones comunes. En este ejemplo, lo aplicaremos en dos subproyectos: app y lib.

Primero, necesitamos crear las carpetas correspondientes para cada subproyecto. Puedes hacerlo con los siguientes comandos según tu sistema operativo:

"src\main\kotlin" | ForEach-Object { 
"app\$_", "lib\$_" | ForEach-Object {
New-Item -Path $_ -ItemType "Directory" -Force
}
}
¿Qué acabamos de hacer?

Este fragmento utiliza dos bucles ForEach-Object anidados para crear de forma concisa los directorios app\src\main\kotlin y lib\src\main\kotlin.

  • En el primer pipeline ("src\main\kotlin"), se define el segmento de ruta común que será añadido a cada módulo.
  • En el segundo pipeline ("app\$_", "lib\$_"), se generan las rutas completas para ambos subproyectos combinando los nombres de los módulos (app, lib) con la ruta común.
  • New-Item crea los directorios y el parámetro -Force asegura que no se generen errores si alguno ya existe.

Este patrón permite mantener el código limpio y fácilmente extensible en caso de que se agreguen más módulos o rutas.

En el subproyecto app:

app/build.gradle.kts
plugins {
id("jvm.conventions")
}

En el subproyecto lib:

lib/build.gradle.kts
plugins {
id("jvm.conventions")
}
¿Qué acabamos de hacer?

Al aplicar el plugin jvm.conventions en los subproyectos app y lib, aseguramos que ambos compartan una base común de configuraciones para proyectos basados en la JVM. En su forma actual, este plugin aplica automáticamente el plugin de Kotlin para JVM y configura la versión del compilador.

A medida que el plugin crezca, podremos extenderlo con reglas, convenciones y dependencias adicionales que se aplicarán de forma coherente en todos los subproyectos, facilitando el mantenimiento y promoviendo la consistencia del proyecto.

🔍 Verificar que el Plugin se Aplica Correctamente

Una vez aplicado el plugin en los subproyectos, puedes verificar que las configuraciones se hayan aplicado correctamente utilizando algunos comandos de Gradle:

.\gradlew --quiet app:dependencies | Select-String "kotlin"
¿Qué acabamos de hacer?

Este comando permite verificar si el plugin kotlin("jvm") fue aplicado correctamente en el subproyecto app.

  • .\gradlew app:dependencies imprime el árbol de dependencias del subproyecto.
  • --quiet reduce el ruido en la salida, omitiendo mensajes como "Task :convention-plugins:checkKotlinGradlePluginConfigurationErrors SKIPPED".
  • | Select-String "kotlin" filtra la salida mostrando solo las líneas relacionadas con Kotlin, lo que permite confirmar si las dependencias del compilador y estándar de Kotlin están presentes.

Esto es útil para validar que el plugin de convenciones está funcionando correctamente.

Nota

Como se usó la opción --quiet, no se mostrarán mensajes durante la ejecución del comando, a menos que haya errores o advertencias. Por eso, podría parecer que no ocurre nada, pero en realidad se está filtrando la salida para mostrar solo las dependencias relacionadas con Kotlin. ¡Paciencia y confianza! 😉

Si la salida del comando muestra dependencias relacionadas con Kotlin, significa que el plugin se aplicó correctamente y que las configuraciones comunes están siendo compartidas entre los subproyectos. En caso contrario, es probable que el plugin no se haya aplicado correctamente, por lo que conviene revisar los pasos anteriores.

🧱 Estructura Final del Proyecto

Después de ejecutar el comando de construcción, tu proyecto debería tener una estructura de directorios similar a la siguiente:

🎯 Conclusiones

En esta lección aprendimos a crear y aplicar un plugin de convenciones en un proyecto Gradle con múltiples módulos. Exploramos cómo encapsular configuraciones comunes dentro de un módulo especializado, reutilizable y fácilmente mantenible.

Este enfoque no solo promueve la coherencia entre los subproyectos, sino que también reduce la repetición, centraliza las decisiones de configuración y facilita el crecimiento del proyecto a medida que evoluciona.

🔑 Puntos clave

  • Un plugin de convención encapsula comportamientos, dependencias y configuraciones comunes que pueden aplicarse automáticamente en múltiples módulos.
  • La instrucción includeBuild("convention-plugins") permite que el módulo esté disponible desde el inicio del proceso de construcción.
  • La combinación de convenciones con un catálogo de versiones (libs.versions.toml) ayuda a mantener dependencias actualizadas, coherentes y fáciles de gestionar.
  • Gradle detecta automáticamente los plugins definidos en archivos como jvm.conventions.gradle.kts si se respeta la convención de nombres.
  • Verificar la aplicación del plugin asegura que el comportamiento esperado se esté aplicando en cada subproyecto.

🧰 ¿Qué nos llevamos?

Adoptar plugins de convención desde las primeras etapas de un proyecto no es solo una cuestión de orden: es una decisión estratégica. Esta práctica convierte decisiones repetitivas en configuraciones automáticas, alivia la carga cognitiva de cada submódulo, y favorece una arquitectura escalable y profesional.

A medida que el proyecto crezca, contar con una base sólida y coherente en su configuración hará una gran diferencia, tanto para quienes lo mantienen como para quienes se incorporen más adelante.

📖 Referencias

🔥 Recomendadas

🔹 Adicionales

  • 📚 Build script essentials. (2014). En B. Muschko & H. Dockter, Gradle in action (pp. 75–104). Manning.