Aprendes comparando con lo que ya sabes. Recientemente me llevé una sorpresa al asumir que Rust funcionaba como Java en cuanto a la resolución de versiones de dependencias transitivas. En esta publicación, quiero comparar los dos.Aprendes comparando con lo que ya sabes. Recientemente me llevé una sorpresa al asumir que Rust funcionaba como Java en cuanto a la resolución de versiones de dependencias transitivas. En esta publicación, quiero comparar los dos.

Resolución de versiones de dependencias transitivas en Rust y Java: Comparando los dos

2025/09/21 23:00

Aprendes comparando con lo que ya sabes. Recientemente me llevé una sorpresa al asumir que Rust funcionaba como Java respecto a la resolución de versiones de dependencias transitivas. En esta publicación, quiero comparar los dos.

Dependencias, Transitividad y Resolución de Versiones

Antes de profundizar en los detalles de cada stack, describamos el dominio y los problemas que conlleva.

\ Al desarrollar cualquier proyecto por encima del nivel Hello World, es probable que te enfrentes a problemas que otros han enfrentado antes. Si el problema está muy extendido, la probabilidad es alta de que alguien haya sido lo suficientemente amable y cívico como para haber empaquetado el código que lo resuelve, para que otros lo reutilicen. Ahora, puedes usar el paquete y concentrarte en resolver tu problema principal. Así es como la industria construye la mayoría de los proyectos hoy en día, incluso si trae otros problemas: te sientas sobre los hombros de gigantes.

\ Los lenguajes vienen con herramientas de construcción que pueden agregar dichos paquetes a tu proyecto. La mayoría de ellos se refieren a los paquetes que agregas a tu proyecto como dependencias. A su vez, las dependencias de los proyectos pueden tener sus propias dependencias: estas últimas se denominan dependencias transitivas.

Transitive dependencies

En el diagrama anterior, C y D son dependencias transitivas.

\ Las dependencias transitivas tienen problemas por sí mismas. El más grande es cuando se requiere una dependencia transitiva desde diferentes rutas, pero en diferentes versiones. En el diagrama a continuación, A y B dependen de C, pero en diferentes versiones.

¿Qué versión de C debería incluir la herramienta de construcción en tu proyecto? Java y Rust tienen respuestas diferentes. Describámoslas por turnos.

Resolución de Versiones de Dependencias Transitivas en Java

Recordatorio: el código Java se compila a bytecode, que luego se interpreta en tiempo de ejecución (y a veces se compila a código nativo, pero esto está fuera de nuestro espacio de problemas actual). Primero describiré la resolución de dependencias en tiempo de ejecución y la resolución de dependencias en tiempo de compilación.

\ En tiempo de ejecución, la Máquina Virtual de Java ofrece el concepto de classpath. Cuando tiene que cargar una clase, el tiempo de ejecución busca a través del classpath configurado en orden. Imagina la siguiente clase:

public static Main {     public static void main(String[] args) {         Class.forName("ch.frankel.Dep");     } } 

\ Vamos a compilarla y ejecutarla:

java -cp ./foo.jar:./bar.jar Main 

\ Lo anterior primero buscará en el foo.jar la clase ch.frankel.Dep. Si la encuentra, se detiene allí y carga la clase, independientemente de si también podría estar presente en el bar.jar; si no, busca más en la clase bar.jar. Si aún no se encuentra, falla con un ClassNotFoundException.

\ El mecanismo de resolución de dependencias en tiempo de ejecución de Java es ordenado y tiene una granularidad por clase. Se aplica tanto si ejecutas una clase Java y defines el classpath en la línea de comandos como se muestra arriba, o si ejecutas un JAR que define el classpath en su manifiesto.

\ Cambiemos el código anterior a lo siguiente:

public static Main {     public static void main(String[] args) {         var dep = new ch.frankel.Dep();     } } 

\ Debido a que el nuevo código hace referencia a Dep directamente, el nuevo código requiere resolución de clases en tiempo de compilación. La resolución del classpath funciona de la misma manera:

javac -cp ./foo.jar:./bar.jar Main 

\ El compilador busca Dep en foo.jar, luego en bar.jar si no lo encuentra. Lo anterior es lo que aprendes al comienzo de tu viaje de aprendizaje de Java.

\ Después, tu unidad de trabajo es el Java Archive, conocido como JAR, en lugar de la clase. Un JAR es un archivo ZIP glorificado, con un manifiesto interno que especifica su versión.

\ Ahora, imagina que eres un usuario de foo.jar. Los desarrolladores de foo.jar establecieron un classpath específico al compilar, posiblemente incluyendo otros JARs. Necesitarás esta información para ejecutar tu propio comando. ¿Cómo pasa un desarrollador de biblioteca este conocimiento a los usuarios posteriores?

\ La comunidad propuso algunas ideas para responder a esta pregunta: La primera respuesta que se mantuvo fue Maven. Maven tiene el concepto de Project Object Model, donde estableces los metadatos de tu proyecto, así como las dependencias. Maven puede resolver fácilmente dependencias transitivas porque también publican su POM, con sus propias dependencias. Por lo tanto, Maven puede rastrear las dependencias de cada dependencia hasta las dependencias hoja.

\ Ahora, volviendo al problema: ¿cómo resuelve Maven los conflictos de versiones? ¿Qué versión de dependencia resolverá Maven para C, 1.0 o 2.0?

\ La documentación es clara: la más cercana.

Dependency resolution with the same dependency in different versions

En el diagrama anterior, la ruta a v1 tiene una distancia de dos, una a B, luego una a C; mientras tanto, la ruta a v2 tiene una distancia de tres, una a A, luego una a D, y finalmente una a C. Por lo tanto, la ruta más corta apunta a v1.

\ Sin embargo, en el diagrama inicial, ambas versiones de C están a la misma distancia del artefacto raíz. La documentación no proporciona respuesta. Si estás interesado en ello, ¡depende del orden de declaración de A y B en el POM! En resumen, Maven devuelve una sola versión de una dependencia duplicada para incluirla en el classpath de compilación.

\ Si A puede trabajar con C v2.0 o B con C 1.0, ¡genial! Si no, probablemente necesitarás actualizar tu versión de A o degradar tu versión de B, para que la versión resuelta de C funcione con ambos. Es un proceso manual que es doloroso, pregúntame cómo lo sé. Peor aún, podrías descubrir que no hay una versión de C que funcione con A y B. Es hora de reemplazar A o B.

Resolución de Versiones de Dependencias Transitivas en Rust

Rust difiere de Java en varios aspectos, pero creo que los siguientes son los más relevantes para nuestra discusión:

  • Rust tiene el mismo árbol de dependencias en tiempo de compilación y en tiempo de ejecución
  • Proporciona una herramienta de construcción lista para usar, Cargo
  • Las dependencias se resuelven desde el código fuente

\ Examinémoslos uno por uno.

\ Java compila a bytecode, luego ejecutas este último. Necesitas establecer el classpath tanto en tiempo de compilación como en tiempo de ejecución. Compilar con un classpath específico y ejecutar con uno diferente puede llevar a errores. Por ejemplo, imagina que compilas con una clase de la que dependes, pero la clase está ausente en tiempo de ejecución. O alternativamente, está presente, pero en una versión incompatible.

\ Contrario a este enfoque modular, Rust compila en un paquete nativo único el código del crate y cada dependencia. Además, Rust proporciona su propia herramienta de construcción, evitando así tener que recordar las peculiaridades de diferentes herramientas. Mencioné Maven, pero otras herramientas de construcción probablemente tienen reglas diferentes para resolver la versión en el caso de uso anterior.

\ Finalmente, Java resuelve dependencias desde binarios: JARs. Por el contrario, Rust resuelve dependencias desde fuentes. En tiempo de compilación, Cargo resuelve todo el árbol de dependencias, descarga todas las fuentes requeridas y las compila en el orden correcto.

\ Con esto en mente, ¿cómo resuelve Rust la versión de la dependencia C en el problema inicial? La respuesta puede parecer extraña si vienes de un entorno Java, pero Rust incluye ambas. De hecho, en el diagrama anterior, Rust compilará A con C v1.0 y compilará B con C v2.0. Problema resuelto.

Conclusión

Los lenguajes JVM, y Java en particular, ofrecen tanto un classpath en tiempo de compilación como un classpath en tiempo de ejecución. Permite modularidad y reutilización, pero abre la puerta a problemas relacionados con la resolución del classpath. Por otro lado, Rust construye tu crate en un único binario autónomo, ya sea una biblioteca o un ejecutable.

\ Para profundizar más:

  • Maven - Introducción al Mecanismo de Dependencias
  • Effective Rust - Ítem 25: Gestiona tu grafo de dependencias

Publicado originalmente en A Java Geek el 14 de septiembre de 2025

Aviso legal: Los artículos republicados en este sitio provienen de plataformas públicas y se ofrecen únicamente con fines informativos. No reflejan necesariamente la opinión de MEXC. Todos los derechos pertenecen a los autores originales. Si consideras que algún contenido infringe derechos de terceros, comunícate con service@support.mexc.com para solicitar su eliminación. MEXC no garantiza la exactitud, la integridad ni la actualidad del contenido y no se responsabiliza por acciones tomadas en función de la información proporcionada. El contenido no constituye asesoría financiera, legal ni profesional, ni debe interpretarse como recomendación o respaldo por parte de MEXC.
Compartir perspectivas

También te puede interesar