wandres.dev
EL LENGUAJE · quince años y un compilador nuevo

El toolchain: compilador, REPL, scripts y Gradle

Instalar Kotlin, compilar desde la línea de comandos, usar el REPL y los scripts .kts, y entender qué hace realmente el DSL de Kotlin para Gradle y qué cuesta usarlo.

⏱ 15 min

Casi todo el mundo conoce Kotlin a través de un IDE que compila por su cuenta y de un archivo de Gradle que alguien copió. Eso funciona hasta el día en que algo falla y no hay un botón para ese error. Bajar al compilador desnudo, ver qué produce, ejecutarlo a mano y solo después subir a la herramienta de construcción es la única forma de tener un modelo mental que aguante bajo presión.

🎯 Al terminar esta lección sabrás
  • Instalar el compilador de Kotlin y comprobar su versión y su destino de bytecode.
  • Compilar y ejecutar código desde la línea de comandos, con y sin biblioteca estándar incluida.
  • Usar el REPL y los scripts .kts para exploración y automatización.
  • Entender qué aporta y qué cuesta el DSL de Kotlin frente al de Groovy en Gradle.

Instalar y comprobar

La instalación recomendada en máquinas de desarrollo es mediante un gestor de versiones, porque necesitarás cambiar de versión de Kotlin con más frecuencia de la que crees.

# opción portable entre macOS y Linux
curl -s https://get.sdkman.io | bash
sdk install kotlin
sdk install kotlin 2.4.0     # una versión concreta
sdk use kotlin 2.4.0         # solo en esta sesión

# alternativa en macOS
brew install kotlin

# comprobación
kotlinc -version

Kotlin no trae su propia máquina virtual: para el objetivo JVM necesitas un JDK instalado. La versión 2.4 admite generar bytecode hasta Java 26, pero eso es independiente del JDK con el que compilas. Conviene tener claros los dos ejes desde el principio, porque confundirlos produce errores desconcertantes en tiempo de ejecución.

kotlinc hola.kt -jvm-target 21 -d salida/

El destino de bytecode define qué máquina virtual podrá ejecutar el resultado. El JDK define qué API tienes disponible al compilar. Pedir un destino antiguo mientras usas una API nueva compila sin quejas y falla al ejecutar en la máquina antigua.

Compilar y ejecutar a mano

Un archivo mínimo y su ciclo completo, sin herramienta de construcción de por medio:

// hola.kt
fun main(args: Array<String>) {
    val quien = args.firstOrNull() ?: "mundo"
    println("Hola, $quien")
}
# 1. compilar a clases sueltas
kotlinc hola.kt -d clases/

# ejecutar necesita la biblioteca estándar en el classpath
kotlin -classpath clases/ HolaKt mundo

# 2. o empaquetar todo en un jar autocontenido
kotlinc hola.kt -include-runtime -d hola.jar
java -jar hola.jar mundo

Dos detalles con consecuencias. El primero: el nombre de la clase generada para un archivo de funciones de nivel superior es el nombre del archivo en mayúsculas iniciales más el sufijo Kt. Eso se puede cambiar con la anotación de nombre para la JVM, y es exactamente lo que hace incómodo llamar a código Kotlin desde Java si no lo controlas. El segundo: la opción que incluye el entorno de ejecución mete la biblioteca estándar dentro del jar, lo que es cómodo para un ejecutable y una mala idea para una biblioteca, donde debe ser una dependencia declarada.

El REPL y los scripts

El REPL se lanza ejecutando el compilador sin argumentos. Sirve para lo que sirve un REPL con tipado estático: verificar una firma, inspeccionar una inferencia, probar una expresión antes de escribirla en el sitio serio.

kotlinc
# >>> val nums = listOf(3, 1, 2)
# >>> nums.sortedDescending()
# res1: kotlin.collections.List<kotlin.Int> = [3, 2, 1]

Los scripts son la parte más infravalorada del toolchain. Un archivo .kts es Kotlin con las declaraciones de nivel superior ejecutándose directamente, sin función principal. Y con la variante .main.kts puedes declarar dependencias externas dentro del propio archivo, lo que convierte a Kotlin en una alternativa con tipos a los scripts de shell para tareas de automatización.

// limpiar.main.kts
@file:DependsOn("com.squareup.okio:okio:3.9.0")

import java.io.File

val raiz = File(args.firstOrNull() ?: ".")
raiz.walkTopDown()
    .filter { it.isFile && it.extension == "tmp" }
    .forEach { println("borrando ${it.path}"); it.delete() }
kotlin limpiar.main.kts ./build

El precio es la latencia de arranque: cada ejecución compila el script, aunque hay caché de compilación entre invocaciones. Para una tarea que se ejecuta cien veces al día en un bucle interactivo, un script de shell sigue ganando; para una tarea de mantenimiento con lógica no trivial, tener tipos y refactorización compensa con creces.

Gradle con el DSL de Kotlin

Gradle ofrece dos lenguajes para sus archivos de construcción, y la diferencia no es estética. Con el DSL de Groovy los archivos se interpretan de forma dinámica, así que el editor adivina y los errores aparecen en tiempo de ejecución. Con el DSL de Kotlin el archivo build.gradle.kts es un programa Kotlin compilado: hay autocompletado real, navegación al código del complemento y errores de tipo antes de ejecutar nada.

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.4.0"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
    testImplementation(kotlin("test"))
}

kotlin {
    jvmToolchain(21)
}

application {
    mainClass.set("com.ejemplo.AppKt")
}

tasks.test {
    useJUnitPlatform()
}

La llamada a la cadena de herramientas de la JVM merece atención aparte: le dice a Gradle qué JDK usar para compilar y ejecutar, descargándolo si hace falta, con independencia del JDK con el que se lanzó Gradle. Es la forma correcta de conseguir que la construcción sea reproducible entre máquinas, y sustituye a la costumbre frágil de fijar solo el destino de bytecode.

El coste del DSL de Kotlin es la compilación de los propios scripts de construcción: la primera configuración tras un cambio es más lenta que con Groovy. A cambio, con la caché de configuración activada y los archivos estables, esa penalización desaparece en el uso diario. Para proyectos con lógica de construcción compartida, la vía madura es extraerla a complementos precompilados en un módulo de lógica de construcción, en lugar de acumular condicionales en un archivo raíz gigante.

La herramienta que no entiendes te acabará gobernando

Existe una asimetría cruel en el trabajo con la JVM: el noventa por ciento del tiempo la herramienta de construcción es invisible y el diez por ciento restante consume días enteros. Y ese diez por ciento se resuelve siempre con el mismo tipo de conocimiento, que es exactamente el que un IDE te ahorra tener. Cuando una dependencia trae una versión distinta de la biblioteca estándar, cuando el bytecode generado no arranca en el servidor, cuando un complemento de compilador no se aplica al conjunto de fuentes correcto, la única salida es saber qué hace el compilador por debajo, en qué se traducen tus funciones de nivel superior y qué diferencia hay entre el JDK que compila y el destino que se emite. Por eso el orden de esta lección es el correcto y no el cómodo: primero kotlinc sobre un archivo, después el jar, después el script, y solo al final Gradle. Quien aprende en ese orden lee un error de construcción como quien lee una traza; quien empieza por copiar un archivo de Gradle ajeno tiene un sistema de producción cuyo comportamiento no puede explicar, y esa deuda se cobra siempre el día del despliegue.

⚔️ El ciclo completo, sin IDE
  1. Instala Kotlin con un gestor de versiones y comprueba que puedes alternar entre dos versiones distintas del compilador en la misma máquina.
  2. Compila un archivo con una función de nivel superior, localiza el nombre exacto de la clase generada y ejecútala con el comando de Java directamente.
  3. Repite la compilación fijando un destino de bytecode antiguo mientras usas una API reciente, y observa en qué momento aparece el fallo.
  4. Escribe un script .main.kts que declare al menos una dependencia externa y resuelva una tarea real de tu flujo de trabajo.
  5. Convierte un archivo de construcción de Groovy a build.gradle.kts, mide el tiempo de configuración antes y después, y extrae la lógica repetida a un complemento precompilado.