El ciclo de compilación: artefactos, metadatos y publicación
Un proyecto multiplataforma no produce un artefacto sino una familia de ellos, más un metadato que explica cómo elegir entre sus miembros. Esta lección recorre lo que emite cada objetivo, el papel del formato intermedio de bibliotecas y de la compilación de metadatos del código común, cómo se publica una biblioteca multiplataforma en forma de módulo raíz con módulos por objetivo, y qué significa exactamente el estado de producción de la tecnología en 2026.
Las cuatro lecciones anteriores describen el modelo desde el lado del autor: dónde vive cada archivo, qué ve cada compilación, cómo se rellenan los agujeros. Falta el lado del consumidor, que es donde el modelo se pone a prueba de verdad, porque un proyecto multiplataforma no publica un artefacto sino un conjunto de artefactos hermanos más un documento que explica cuál corresponde a cada situación. Ese documento es el metadato de módulo, y sin él la resolución de dependencias es imposible: un consumidor que compila para un procesador de Apple y otro que compila para la máquina virtual piden literalmente la misma coordenada de grupo, nombre y versión, y esperan recibir binarios completamente distintos. Comprender el ciclo completo —qué emite cada objetivo, qué es exactamente lo que viaja en el módulo raíz y cómo elige el consumidor— es lo que separa a quien usa bibliotecas multiplataforma de quien puede publicarlas, y también lo que convierte los errores de resolución más opacos del ecosistema en mensajes que se leen de un vistazo.
- Enumerar qué artefacto produce cada familia de objetivos y en qué fases se produce.
- Explicar el papel del formato intermedio de bibliotecas y de la compilación de metadatos del código común.
- Describir la estructura de una publicación multiplataforma y cómo el consumidor selecciona la variante correcta.
- Situar con precisión qué cubre la estabilidad declarada de la tecnología en 2026 y qué sigue siendo terreno en movimiento.
Qué emite cada objetivo
La primera diferencia estructural es que no todos los backends producen su binario final en una sola fase. El objetivo de la máquina virtual es el más directo: compila a bytecode y lo empaqueta en un archivo comprimido corriente, indistinguible del que produciría un proyecto de un solo objetivo. Android añade la dimensión de las variantes de compilación, pero el principio es el mismo.
Conviene fijar antes el vocabulario, porque los mensajes de la herramienta lo usan sin explicarlo. Cada objetivo declara al menos dos compilaciones, la principal y la de pruebas, y cada compilación tiene un nombre compuesto que aparece literalmente en las tareas y en los errores. Aprender a leer ese nombre —qué objetivo, qué compilación— convierte un muro de texto en una indicación precisa de dónde mirar, y es probablemente la habilidad de diagnóstico más rentable de todo el nivel.
// Nombres de tarea que conviene reconocer al leer un fallo
// compileKotlinJvm principal del objetivo de la maquina virtual
// compileTestKotlinJvm pruebas del mismo objetivo
// compileKotlinIosArm64 principal de un objetivo nativo
// compileCommonMainKotlinMetadata la compilacion de metadatos del comun
Los backends de Kotlin/Native, de JavaScript y de WebAssembly siguen en cambio un camino de dos fases. Primero producen una biblioteca en un formato intermedio propio que contiene la representación intermedia serializada y los metadatos de las declaraciones; no es código máquina y no se puede ejecutar. Después, una fase de enlazado toma esa biblioteca junto con sus dependencias y produce el binario final para la arquitectura pedida: un marco de trabajo o una biblioteca estática en el mundo de Apple, un módulo de JavaScript, un módulo de WebAssembly. Esa separación explica dos hechos que confunden al principiante: que compilar sea rápido y enlazar lento, y que una biblioteca publicada para nativo no contenga código máquina sino una forma intermedia que se compilará realmente en la máquina de quien la consume.
// Lo que ocurre por objetivo, expresado como comentario
// jvm -> bytecode -> jar
// androidTarget -> bytecode por variante -> aar
// iosArm64 -> biblioteca intermedia -> framework tras enlazar
// js -> biblioteca intermedia -> modulo de JavaScript
// wasmJs -> biblioteca intermedia -> modulo de WebAssembly
Queda una compilación más, que no corresponde a ningún objetivo y que mucha gente descubre solo al ver su nombre en un registro de errores: la de metadatos. El código común y los conjuntos intermedios se compilan también por su cuenta para producir un artefacto que no se ejecuta en ninguna parte y que contiene únicamente declaraciones. Sirve para dos cosas imprescindibles: que el entorno de desarrollo pueda analizar el código común sin elegir un objetivo, y que otro proyecto multiplataforma que dependa del tuyo pueda ver tus declaraciones desde su propio código común.
Ese artefacto de metadatos es la razón última por la que una biblioteca multiplataforma no se puede consumir desde el conjunto común si solo publicó variantes por objetivo. Sin declaraciones comunes no hay nada que el analizador pueda mirar al compilar la raíz de tu grafo, y el error que recibe quien lo intenta habla de un símbolo no resuelto en una plataforma cuando el problema real es que falta una pieza entera de la publicación.
En los backends de dos fases, un fallo al compilar habla de tu código y un fallo al enlazar habla de tu grafo de dependencias: símbolos duplicados, versiones incompatibles o una biblioteca que no se compiló para esa arquitectura. Distinguirlos ahorra la mitad del tiempo de diagnóstico.
Conviene añadir un matiz sobre el enlazado nativo porque explica una experiencia frustrante muy común. Producir el binario de depuración es relativamente rápido, mientras que el de publicación aplica optimizaciones globales sobre todo el programa, incluida la eliminación agresiva de código inalcanzable, y por eso puede tardar órdenes de magnitud más. No es una patología: es la contrapartida de que el resultado sea código máquina sin capa intermedia, y la razón por la que en un proyecto sano las compilaciones de publicación no se ejecutan en cada cambio sino en la integración.
flowchart TD A[commonMain] --> B[Compilacion de metadatos] B --> C[Artefacto de metadatos con declaraciones] A --> D[Compilacion por objetivo] D --> E[Bytecode] D --> F[Biblioteca intermedia nativa] F --> G[Enlazado y binario final] D --> H[Biblioteca intermedia de JS o Wasm] C --> I[Modulo raiz] E --> J[Modulo jvm] G --> K[Modulo iosarm64] H --> L[Modulo js] I --> M[El consumidor elige por atributos]
La publicación: un módulo raíz y una familia de variantes
Publicar un proyecto multiplataforma produce más de un módulo. Hay uno por objetivo, con las coordenadas del proyecto y un sufijo que lo identifica, y hay uno raíz sin sufijo que contiene el artefacto de metadatos y, sobre todo, la lista de todos sus hermanos con los atributos que describen para qué sirve cada uno. Ese módulo raíz es el que declaras como dependencia; el resto lo resuelve la herramienta de compilación.
La pieza que hace funcionar el mecanismo es el metadato de módulo enriquecido, un formato que acompaña al descriptor tradicional y que puede expresar algo que aquel nunca pudo: que un mismo componente tiene varias variantes y que cada una lleva atributos como el tipo de plataforma, la arquitectura o el modo de uso. Cuando tu proyecto pide la dependencia, la resolución compara los atributos que tu compilación necesita con los que cada variante ofrece y selecciona la única compatible. De ahí la consecuencia práctica más importante para quien publica: si el repositorio o el proceso de publicación descarta ese metadato enriquecido, la biblioteca queda inservible para consumidores multiplataforma aunque los archivos estén todos ahí.
// El consumidor declara una sola coordenada, en el conjunto común
commonMain.dependencies {
implementation("org.ejemplo:mi-libreria:1.4.0")
}
// y cada compilación recibe la variante que le corresponde
Esa asimetría entre lo que se escribe y lo que ocurre es la esencia del mecanismo: una línea en el conjunto común se traduce en tantas resoluciones distintas como objetivos tenga el proyecto, y cada una puede acabar descargando un artefacto diferente. Cuando funciona resulta invisible; cuando falla, el mensaje habla de atributos y de variantes candidatas, que es un vocabulario que conviene reconocer antes de necesitarlo.
Hay una consecuencia adicional para quien publica y que se descubre tarde: el conjunto de objetivos de una biblioteca forma parte de su interfaz pública tanto como sus firmas. Quitar un objetivo en una versión menor rompe a todo consumidor que lo usara, y ni el compilador ni el gestor de dependencias lo señalarán como un cambio incompatible, porque desde su punto de vista simplemente no hay variante. Añadirlos es seguro; retirarlos exige el mismo cuidado que retirar una función pública.
Conviene mencionar además que el objetivo de Android tiene reglas propias por su sistema de variantes de compilación, y que la interoperabilidad con el mundo de Apple ha ganado una vía adicional: además del marco de trabajo tradicional y de la distribución mediante paquetes binarios, Kotlin 2.4 incorporó el soporte de paquetes de Swift como dependencias, de modo que la cadena de suministro deja de ser exclusivamente unidireccional.
Cuando la herramienta dice que no encuentra una variante compatible, rara vez falta un archivo. Lo que ocurre es que ninguna de las variantes publicadas declara los atributos que tu compilación pide, normalmente porque la biblioteca no cubre ese objetivo o porque se publicó sin metadato enriquecido.
Esa arquitectura tiene otra consecuencia que conviene tener presente al planificar: la alineación de versiones deja de ser opcional. Todas las variantes de un componente comparten versión por construcción, y las bibliotecas del ecosistema declaran restricciones para que no puedas mezclar versiones incompatibles del tiempo de ejecución de corrutinas o de serialización entre objetivos. Un conflicto que en un proyecto de una sola plataforma se resolvería en silencio con la versión más alta, aquí se manifiesta como un fallo explícito, y es preferible que así sea.
Qué significa producción en 2026
Conviene ser preciso con las fechas porque el discurso de madurez lleva años repitiéndose y no siempre con el mismo contenido. La tecnología es estable desde noviembre de 2023, y esa estabilidad se refiere al lenguaje, al modelo de compilación y a las herramientas, no a que cada formato intermedio vaya a permanecer inmutable para siempre. Compose Multiplatform alcanzó la estabilidad para iOS en mayo de 2025 con la versión 1.8.0, que es la fecha en la que compartir también la capa de presentación dejó de ser una apuesta técnica. En 2026 la recarga en caliente es estable, las compilaciones son alrededor de un veinticinco por ciento más rápidas consumiendo menos de la mitad de memoria, y el desplazamiento y la entrada de texto en iOS se comportan como los nativos, que era la última costura perceptible. Kotlin 2.4 añadió el soporte de paquetes de Swift como dependencias y mejoras a la exportación hacia Swift, con lo que la frontera con ese lenguaje deja de ser un puente de un solo sentido.
Esa lista de mejoras tiene un hilo común que merece nombrarse, porque no es casual: todas atacan el bucle de trabajo, no la capacidad. La tecnología ya podía hacer en 2024 casi todo lo que hace hoy; lo que ha cambiado es cuánto tarda un desarrollador en ver el efecto de un cambio, que es la variable que decide si un equipo adopta algo o lo abandona a los tres meses. Recarga en caliente estable, compilaciones más rápidas y con menos memoria, y comportamiento nativo en el desplazamiento y la entrada de texto son, los tres, mejoras de bucle. El indicador de madurez de una tecnología multiplataforma nunca fue la lista de lo que se puede hacer, sino el tiempo entre escribir y comprobar.
También conviene leer los casos de producción con el detalle correcto. Lo relevante de Netflix, McDonald’s y Cash App no es que usen la tecnología, sino que llevan años usándola y que ninguno de los tres empezó reescribiendo su aplicación: los tres extendieron aplicaciones nativas existentes con módulos compartidos, que es exactamente el modo de adopción que el modelo favorece. Un caso de éxito que exige empezar de cero demuestra bastante poco; uno que sobrevive dentro de una base de código antigua demuestra que la frontera funciona.
Y hay que ser igual de preciso con lo que la estabilidad no cubre, porque el discurso de madurez tiende a redondear. Los formatos intermedios que producen los backends no ejecutables siguen siendo una zona en evolución, con trabajo activo sobre su compatibilidad entre versiones; eso no afecta al código que escribes, pero sí significa que una biblioteca compilada con una versión antigua del compilador puede necesitar republicarse. La regla operativa que se deduce es sencilla y vale la pena adoptarla desde el primer día: alinea la versión del compilador entre todos los módulos del proyecto y trata las actualizaciones de la cadena de herramientas como un cambio coordinado y no como una dependencia más.
La declaración de estabilidad es una promesa sobre compatibilidad hacia adelante del lenguaje y del modelo, no una afirmación de que nada volverá a cambiar. Lo que sí garantiza es que los cambios que lleguen tendrán una ruta de migración y no exigirán reescribir.
Puestos a resumir el ciclo entero en una frase útil para el trabajo diario: escribes un árbol de fuentes, el compilador lo recorre una vez por objetivo más una vez para los metadatos, cada recorrido produce un artefacto de naturaleza distinta, y la publicación empaqueta esa familia junto con el documento que enseña a elegir entre sus miembros. Todo lo demás de este nivel —los conjuntos de fuentes, las declaraciones esperadas, las interfaces inyectadas— son decisiones sobre qué contiene ese árbol y sobre dónde se abren sus agujeros.
Netflix
Lleva años con lógica compartida en producción, con la costura puesta deliberadamente por debajo de la interfaz de cada sistema.
McDonald's
Caso conocido de adopción incremental sobre aplicaciones existentes, módulo a módulo y sin reescritura.
Cash App
Uno de los adoptantes más antiguos y también uno de los mayores contribuyentes de bibliotecas del ecosistema.
Kotlin 2.4
Paquetes de Swift como dependencias y exportación hacia Swift mejorada: la interoperabilidad deja de ser unidireccional.
Los debates sobre madurez tecnológica se libran casi siempre en el terreno equivocado: rendimiento, calidad del código generado, fidelidad visual. Son preguntas legítimas y también las más fáciles de responder, porque se miden con un banco de pruebas en una tarde. La pregunta difícil, la que decide de verdad el destino de un proyecto a cinco años, es de otro orden y se parece más a la logística que a la ingeniería: cuando aparezca una plataforma nueva, o una arquitectura nueva, o simplemente una versión del sistema operativo que rompa una interfaz, cuántos actores tienen que moverse para que tu programa vuelva a compilar, y en qué orden. En un modelo con máquina virtual embebida la respuesta es corta porque casi todo depende de un solo proveedor, con lo que ganas simplicidad y pierdes autonomía. En un modelo como este la respuesta es larga: se mueve el compilador, se mueven las bibliotecas de plataforma, se mueve cada biblioteca de terceros que hayas usado, y solo cuando la última haya publicado una variante para el objetivo nuevo estará tu grafo de dependencias completo otra vez. Por eso el hecho verdaderamente relevante de 2026 no es que la tecnología sea estable —lo es desde 2023— sino que el ecosistema tenga profundidad suficiente para que esa cadena se mueva sola: bibliotecas mantenidas por equipos distintos, adoptantes grandes con años de producción a sus espaldas cuyo interés comercial coincide con el de mantenerlas vivas, y una progresión demostrada de incorporación de objetivos nuevos. Ahí es donde adquiere su sentido pleno la lección anterior sobre interfaces frente a declaraciones esperadas: cada interfaz de tu diseño es un punto donde puedes completar la cadena tú mismo si alguien se queda atrás, y cada declaración esperada publicada por otro es un eslabón que solo esa persona puede forjar. La arquitectura que resiste al tiempo no es la que comparte más código, sino la que deja en tus manos el mayor número de eslabones.
- Publica un módulo multiplataforma mínimo en un repositorio local y enumera todos los módulos generados. Identifica el raíz y ábrelo para leer la lista de variantes.
- Localiza en esa lista los atributos que distinguen cada variante y explica cuál se seleccionaría desde una compilación para la máquina virtual y cuál desde una nativa.
- Consúmelo desde otro proyecto multiplataforma declarando la dependencia solo en el conjunto común y comprueba que cada objetivo recibe su artefacto.
- Publica una segunda versión sin uno de los objetivos y observa el mensaje exacto de resolución que recibe el consumidor. Guárdalo como referencia para diagnósticos futuros.
- Recorre tu grafo de dependencias y anota qué bibliotecas no cubren alguno de los objetivos que planeas añadir. Ese listado es tu riesgo real de cadena de suministro.