Herramienta de resolución de dependencias
Un solucionador de dependencias (en adelante denominado resolutor, aprox. Transl.) O un administrador de paquetes es un programa que define un conjunto consistente de módulos teniendo en cuenta las restricciones establecidas por el usuario.
Las restricciones generalmente se especifican por los nombres de los módulos y los números de versión. En el ecosistema JVM para módulos Maven, también se indicará el nombre de la organización (identificación del grupo). Además, las restricciones pueden incluir rangos de versión, módulos excluidos, anulaciones de versión, etc.
Las tres categorías principales de paquetes están representadas por los paquetes del sistema operativo (Homebrew, paquetes Debian, etc.),
módulos para lenguajes de programación específicos (CPAN, RubyGem, Maven, etc.) y extensiones específicas de la aplicación (complementos Eclipse, complementos IntelliJ, extensiones de código VS).
Semántica de resolución
En una primera aproximación, podemos representar las dependencias de los módulos como un DAG (gráfico acíclico dirigido, gráfico acíclico dirigido).
Esta representación se llama el gráfico de dependencia. Considere las dependencias de dos módulos:
a:1.0
depende de c:1.0
b:1.0
depende de c:1.0
d:1.0
+-----+ +-----+ |a:1.0| |b:1.0| +--+--+ +--+--+ | | +<-------+ | | vv +--+--+ +--+--+ |c:1.0| |d:1.0| +-----+ +-----+
Si el módulo depende de a:1.0
b:1.0
, se presentará una lista completa de dependencias a:1.0
, b:1.0
, c:1.0
d:1.0
. Y esto es solo un recorrido por el árbol.
La situación se volverá más complicada si las dependencias transitivas se especifican por un rango de versiones:
a:1.0
depende de c:1.0
b:1.0
depende de c:[1.0,2)
d:1.0
+-----+ +-----+ |a:1.0| |b:1.0| +--+--+ +--+--+ | | | +-----------+ | | | vvv +--+--+ +--+------+ +--+--+ |c:1.0| |c:[1.0,2)| |d:1.0| +-----+ +---------+ +-----+
O, si se especifican diferentes versiones para dependencias transitivas:
a:1.0
depende de c:1.0
b:1.0
depende de c:1.2
d:1.2
O, si se lanzan excepciones para la dependencia:
- dependencia de
a:1.0
, que depende de c:1.0
, excluyendo c:*
b:1.0
depende de c:1.2
d:1.2
Los diferentes resolutores interpretan las restricciones establecidas por los usuarios de manera diferente. Llamo a tales reglas la semántica de los resolutores.
Es posible que necesite conocer algunas de estas semánticas, por ejemplo:
- semántica de su propio módulo (determinado por la herramienta de compilación que utiliza);
- la semántica de las bibliotecas que usa (determinada por la herramienta de compilación que utilizó el autor);
- la semántica de los módulos que su módulo usará como dependencia (definida por la herramienta de compilación del usuario final).
Herramientas de resolución de dependencias en el ecosistema JVM
Como apoyo a sbt
, tengo que trabajar principalmente en el ecosistema JVM.
Semántica Maven: victorias más cercanas
En los gráficos donde hay un conflicto de dependencias (en el gráfico de dependencia a
hay muchas versiones diferentes del componente d
, por ejemplo d:1.0
d:2.0
), Maven utiliza la estrategia de victorias más cercana para resolver el conflicto.
La resolución de conflictos de dependencias es un proceso que determina qué versión de un artefacto se seleccionará si se encuentran varias versiones diferentes del mismo artefacto entre las dependencias. Maven selecciona la definición más cercana. Es decir usa la versión más cercana a su proyecto en el árbol de dependencias.
Siempre puede garantizar el uso de la versión correcta al declararla explícitamente en el POM del proyecto. Tenga en cuenta que si dos versiones de la dependencia tienen la misma profundidad en el árbol, se seleccionará la primera. La definición más cercana significa que se utilizará la versión más cercana al proyecto en el árbol de dependencias. Por ejemplo, si las dependencias para A
, B
y C
definen como A -> B -> C -> D 2.0
y A -> E -> D 1.0
, entonces, al construir A
, D 1.0
se usará, porque la ruta de A
a D
través de E
más corta (que a través de B
y C
, aprox. transl.).
Esto significa que muchos módulos Java publicados utilizando Maven se compilaron utilizando la semántica nearest-wins
. Para ilustrar lo anterior, cree un pom.xml
simple:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>foo</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <dependencyManagement> <dependencies> <dependency> <groupId>com.typesafe.play</groupId> <artifactId>play-ws-standalone_2.12</artifactId> <version>1.0.1</version> </dependency> </dependencies> </dependencyManagement> </project>
mvn dependency:build-classpath
devuelve el classpath
resuelto.
Es de destacar que el árbol resultante utiliza com.typesafe:config:1.2.0
a pesar de que Akka 2.5.3
depende transitivamente de com.typesafe:config:1.3.1
.
mvn dependency:tree
da esa confirmación visual:
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ foo --- [INFO] com.example:foo:jar:1.0.0 [INFO] \- com.typesafe.play:play-ws-standalone_2.12:jar:1.0.1:compile [INFO] +- org.scala-lang:scala-library:jar:2.12.2:compile [INFO] +- javax.inject:javax.inject:jar:1:compile [INFO] +- com.typesafe:ssl-config-core_2.12:jar:0.2.2:compile [INFO] | +- com.typesafe:config:jar:1.2.0:compile [INFO] | \- org.scala-lang.modules:scala-parser-combinators_2.12:jar:1.0.4:compile [INFO] \- com.typesafe.akka:akka-stream_2.12:jar:2.5.3:compile [INFO] +- com.typesafe.akka:akka-actor_2.12:jar:2.5.3:compile [INFO] | \- org.scala-lang.modules:scala-java8-compat_2.12:jar:0.8.0:compile [INFO] \- org.reactivestreams:reactive-streams:jar:1.0.0:compile
Muchas bibliotecas ofrecen compatibilidad con versiones anteriores, pero la compatibilidad directa no está garantizada con algunas excepciones, lo cual es alarmante.
Semántica de Apache Ivy: últimos triunfos
De forma predeterminada, Apache Ivy utiliza la estrategia de los últimos triunfos para resolver conflictos de dependencia.
Si este contenedor no está presente, el administrador de conflictos predeterminado se usa para todos los módulos. El administrador de conflictos predeterminado actual es "última revisión".
Nota: Los conflicts
contenedor son uno de los archivos Ivy.
Hasta la versión SBT 1.3.x
dependencias interno es Apache Ivy. El pom.xml
utilizado anteriormente se describe en SBT un poco más brevemente:
ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies += "com.typesafe.play" %% "play-ws-standalone" % "1.0.1", )
En el sbt shell
ingrese show externalDependencyClasspath
para obtener el classpath resuelto. Debe indicar la versión de com.typesafe:config:1.3.1
. Además, se mostrará la siguiente advertencia:
[warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings.
Llamar al evicted
en el sbt shell
permite obtener un informe de resolución de conflictos:
sbt:foo> evicted [info] Updating ... [info] Done updating. [info] Here are other dependency conflicts that were resolved: [info] * com.typesafe:config:1.3.1 is selected over 1.2.0 [info] +- com.typesafe.akka:akka-actor_2.12:2.5.3 (depends on 1.3.1) [info] +- com.typesafe:ssl-config-core_2.12:0.2.2 (depends on 1.2.0) [info] * com.typesafe:ssl-config-core_2.12:0.2.2 is selected over 0.2.1 [info] +- com.typesafe.play:play-ws-standalone_2.12:1.0.1 (depends on 0.2.2) [info] +- com.typesafe.akka:akka-stream_2.12:2.5.3 (depends on 0.2.1)
En la semántica latest-wins
, especificar config:1.2.0
en la práctica significa "proporcionarme la versión 1.2.0 o superior".
Este comportamiento es ligeramente más preferible que en la estrategia de nearest-wins
, porque las versiones de las bibliotecas transitivas no se degradan. Sin embargo, la llamada evicted
debe verificar si los reemplazos se realizaron correctamente.
Semántica Coursier: últimas victorias
Antes de abordar la descripción de la semántica, responderé una pregunta importante: cómo se pronuncia Coursier. Según la nota de Alex Arshambo , se pronuncia chick-sie .
Curiosamente, la documentación para Coursier tiene una página sobre versiones , que habla sobre la semántica de resolver dependencias.
Considere la intersección de intervalos dados:
- Si está vacío (los intervalos no se cruzan), entonces hay un conflicto.
- Si no se especifican intervalos, se supone que la intersección está representada por (,) (el intervalo correspondiente a todas las versiones).
Luego, considere versiones específicas:
- Descartamos versiones específicas debajo de los límites del intervalo.
- Si hay versiones específicas por encima de los límites del intervalo, entonces hay un conflicto.
- Si hay versiones específicas dentro de los límites del intervalo, el resultado debería ser el último de ellos.
- Si no hay versiones específicas dentro o por encima de los límites del intervalo, el resultado debería ser el intervalo.
Porque se dice
, por lo tanto, esta es la semántica de los latest-wins
.
Puede verificar esto tomando sbt 1.3.0-RC3
, que usa Coursier.
ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies += "com.typesafe.play" %% "play-ws-standalone" % "1.0.1", )
Llamar a show externalDependencyClasspath
desde la sbt 1.3.0-RC3
devolverá com.typesafe:config:1.3.1
, como se esperaba. El Informe de resolución de conflictos informa lo mismo:
sbt:foo> evicted [info] Here are other dependency conflicts that were resolved: [info] * com.typesafe:config:1.3.1 is selected over 1.2.0 [info] +- com.typesafe.akka:akka-actor_2.12:2.5.3 (depends on 1.3.1) [info] +- com.typesafe:ssl-config-core_2.12:0.2.2 (depends on 1.2.0) [info] * com.typesafe:ssl-config-core_2.12:0.2.2 is selected over 0.2.1 [info] +- com.typesafe.play:play-ws-standalone_2.12:1.0.1 (depends on 0.2.2) [info] +- com.typesafe.akka:akka-stream_2.12:2.5.3 (depends on 0.2.1)
Nota: ¿Apache Ivy emula la semántica de nearest-wins
?
Al resolver las dependencias del módulo del repositorio de Maven, Ivy convierte el archivo POM
y coloca el atributo force="true"
en ivy.xml
en el caché.
Por ejemplo, cat ~/.ivy2/cache/com.typesafe.akka/akka-actor_2.12/ivy-2.5.3.xml
:
<dependencies> <dependency org="org.scala-lang" name="scala-library" rev="2.12.2" force="true" conf="compile->compile(*),master(compile);runtime->runtime(*)"/> <dependency org="com.typesafe" name="config" rev="1.3.1" force="true" conf="compile->compile(*),master(compile);runtime->runtime(*)"/> <dependency org="org.scala-lang.modules" name="scala-java8-compat_2.12" rev="0.8.0" force="true" conf="compile->compile(*),master(compile);runtime->runtime(*)"/> </dependencies>
La documentación de Ivy dice:
Estos dos latest
gestores de conflictos tienen en cuenta el atributo de dependencia de force
.
Por lo tanto, las dependencias directas pueden declarar un atributo de force
(ver dependencia), indicando que de dependencia directa y revisiones indirectas, se debe dar preferencia a las revisiones de dependencia directa.
Para mí, esta formulación significa que force="true"
concibió para redefinir la lógica de las latest-wins
y emular la semántica de las nearest-wins
. Pero, afortunadamente, esto no estaba destinado a suceder, y ahora tenemos los latest-wins
: como podemos ver, sbt 1.2.8
recoge com.typesafe:config:1.3.1
.
Sin embargo, se puede observar el efecto de force="true"
cuando se usa un administrador de conflictos estricto, que parece estar roto.
ThisBuild / conflictManager := ConflictManager.strict
El problema es que un administrador de conflictos estricto no parece evitar la sustitución de versiones. show externalDependencyClasspath
devuelve alegremente com.typesafe:config:1.3.1
.
Un problema relacionado es que agregar una versión de com.typesafe:config:1.3.1
, que un administrador de conflictos estricto puso en el gráfico, conduce a un error.
ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" ThisBuild / conflictManager := ConflictManager.strict lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies ++= List( "com.typesafe.play" %% "play-ws-standalone" % "1.0.1", "com.typesafe" % "config" % "1.3.1", ) )
Se ve así:
sbt:foo> show externalDependencyClasspath [info] Updating ... [error] com.typesafe#config;1.2.0 (needed by [com.typesafe#ssl-config-core_2.12;0.2.2]) conflicts with com.typesafe#config;1.3.1 (needed by [com.example#foo_2.12;1.0.0-SNAPSHOT]) [error] org.apache.ivy.plugins.conflict.StrictConflictException: com.typesafe#config;1.2.0 (needed by [com.typesafe#ssl-config-core_2.12;0.2.2]) conflicts with com.typesafe#config;1.3.1 (needed by [com.example#foo_2.12;1.0.0-SNAPSHOT])
Sobre el versionado
Mencionamos la semántica de latest-wins
, lo que sugiere que las versiones en una representación de cadena pueden ocurrir en algún orden.
Por lo tanto, el control de versiones es parte de la semántica.
Procedimiento de versionado en Apache Ivy
Este comentario de Javadoc dice que al crear el comparador de versiones, Ivy se centró en la función de comparar versiones de PHP :
Esta función primero reemplaza _, - y + con un punto .
en representaciones de cadenas de versiones y también agrega .
antes y después de todo lo que no es un número. Entonces, por ejemplo, '4.3.2RC1' se convierte en '4.3.2.RC.1'. Luego compara las partes recibidas de izquierda a derecha.
Para las partes que contienen elementos especiales ( dev
, alpha
o a
, beta
o b
, RC
o rc
, #
, pl
o p
) *, los elementos se comparan en el siguiente orden:
cualquier cadena que no sea un elemento especial <dev <alpha = a <beta = b <RC = rc <# <pl = p.
Por lo tanto, no solo se pueden comparar diferentes niveles (por ejemplo, '4.1' y '4.1.2'), sino también versiones específicas de PHP que contienen información sobre el estado del desarrollo.
* aprox. perev.
Podemos verificar cómo se ordenan las versiones escribiendo una pequeña función.
scala> :paste
Procedimiento de versionado en Coursier
La página de GitHub sobre semántica de resolución de dependencias tiene una sección sobre versiones.
Coursier utiliza el orden de versiones personalizado de Maven. Antes de comparar, las representaciones de cadena de las versiones se desglosan en elementos separados ...
Para obtener tales elementos, las versiones se separan por los caracteres., - y _ (y los separadores se descartan), y por reemplazos de letra a número o de número a letra.
Para escribir una prueba, cree un subproyecto con libraryDependencies += "io.get-coursier" %% "coursier-core" % "2.0.0-RC2-6"
y ejecute la console
:
sbt:foo> helper/console [info] Starting scala interpreter... Welcome to Scala 2.12.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_212). Type in expressions for evaluation. Or try :help. scala> import coursier.core.Version import coursier.core.Version scala> def sortVersionsCoursier(versions: String*): List[String] = | versions.toList.map(Version.apply).sorted.map(_.repr) sortVersionsCoursier: (versions: String*)List[String] scala> sortVersionsCoursier("1.0", "2.0", "1.0-alpha", "1.0+alpha", "1.0-X1", "1.0a", "2.0.2") res0: List[String] = List(1.0-alpha, 1.0, 1.0-X1, 1.0+alpha, 1.0a, 2.0, 2.0.2)
Como resultado, Coursier ordena los números de versión en un orden completamente diferente al de Ivy.
Si usó etiquetas alfabéticas permisivas, este orden puede causar cierta confusión.
Acerca de los rangos de versión
Por lo general, evito usar rangos de versiones, aunque son ampliamente utilizados en webjars y módulos npm republicados en Maven Central. Algo como "is-number": "^4.0.0"
puede escribirse en el módulo "is-number": "^4.0.0"
que corresponderá a [4.0.0,5)
.
Manejo del rango de versiones en Apache Ivy
En este ensamblaje, angular-boostrap:0.14.2
depende de angular:[1.3.0,)
.
ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies ++= List( "org.webjars.bower" % "angular" % "1.4.7", "org.webjars.bower" % "angular-bootstrap" % "0.14.2", ) )
Llamar a show externalDependencyClasspath
en sbt 1.2.8
devolverá angular-bootstrap:0.14.2
y angular:1.7.8
. ¿Y a dónde fue 1.7.8
? Cuando Ivy se encuentra con una variedad de versiones, esencialmente va a Internet y encuentra lo que puede encontrar, a veces incluso usando screencraping.
Este procesamiento de rangos de versiones hace que los ensamblajes no se repitan (ejecutar el mismo ensamblaje una vez cada pocos meses le brinda resultados diferentes).
Manejo de rangos de versiones en Coursier
Sección de resolución de dependencia de Coursier en la página de Github
lee:
Se prefieren versiones específicas a intervalos
Si su módulo tiene una dependencia de [1.0,2.0) y 1.4, la aprobación de la versión se realizará a favor de 1.4.
Si hay una dependencia de 1.4, entonces esta versión será preferida en el rango [1.0,2.0).
Se ve prometedor.
sbt:foo> show externalDependencyClasspath [warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings. [info] * Attributed(/Users/eed3si9n/.sbt/boot/scala-2.12.8/lib/scala-library.jar) [info] * Attributed(/Users/eed3si9n/.coursier/cache/v1/https/repo1.maven.org/maven2/org/webjars/bower/angular/1.4.7/angular-1.4.7.jar) [info] * Attributed(/Users/eed3si9n/.coursier/cache/v1/https/repo1.maven.org/maven2/org/webjars/bower/angular-bootstrap/0.14.2/angular-bootstrap-0.14.2.jar)
show externalDependencyClasspath
en el mismo ensamblado con angular-bootstrap:0.14.2
devuelve angular-bootstrap:0.14.2
y angular:1.4.7
como se esperaba. Esta es una mejora sobre Ivy.
Considere el caso más complicado cuando se utilizan múltiples rangos de versiones disjuntas. Por ejemplo:
ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies ++= List( "org.webjars.npm" % "randomatic" % "1.1.7", "org.webjars.npm" % "is-odd" % "2.0.0", ) )
Llamar a show externalDependencyClasspath
en sbt 1.3.0-RC3
devuelve el siguiente error:
sbt:foo> show externalDependencyClasspath [info] Updating https://repo1.maven.org/maven2/org/webjars/npm/kind-of/maven-metadata.xml No new update since 2018-03-10 06:32:27 https://repo1.maven.org/maven2/org/webjars/npm/is-number/maven-metadata.xml No new update since 2018-03-09 15:25:26 https://repo1.maven.org/maven2/org/webjars/npm/is-buffer/maven-metadata.xml No new update since 2018-08-17 14:21:46 [info] Resolved dependencies [error] lmcoursier.internal.shaded.coursier.error.ResolutionError$ConflictingDependencies: Conflicting dependencies: [error] org.webjars.npm:is-number:[3.0.0,4):default(compile) [error] org.webjars.npm:is-number:[4.0.0,5):default(compile) [error] at lmcoursier.internal.shaded.coursier.Resolve$.validate(Resolve.scala:394) [error] at lmcoursier.internal.shaded.coursier.Resolve.validate0$1(Resolve.scala:140) [error] at lmcoursier.internal.shaded.coursier.Resolve.$anonfun$ioWithConflicts0$4(Resolve.scala:184) [error] at lmcoursier.internal.shaded.coursier.util.Task$.$anonfun$flatMap$2(Task.scala:14) [error] at scala.concurrent.Future.$anonfun$flatMap$1(Future.scala:307) [error] at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:41) [error] at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64) [error] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [error] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [error] at java.lang.Thread.run(Thread.java:748) [error] (update) lmcoursier.internal.shaded.coursier.error.ResolutionError$ConflictingDependencies: Conflicting dependencies: [error] org.webjars.npm:is-number:[3.0.0,4):default(compile) [error] org.webjars.npm:is-number:[4.0.0,5):default(compile)
Técnicamente, es correcto, porque estos rangos no se superponen. Mientras que sbt 1.2.8
resuelve esto a is-number:4.0.0
.
Debido al hecho de que los rangos de versiones son lo suficientemente comunes como para ser molestos, envío una solicitud de extracción al servicio de mensajería para implementar reglas semánticas adicionales de latest-wins
que le permiten seleccionar versiones posteriores de los límites inferiores de los rangos.
Ver coursier / coursier # 1284 .
Conclusión
La semántica del resolutor define una ruta de clase específica basada en restricciones definidas por el usuario.
Por lo general, las diferencias en los detalles se manifiestan en diferentes formas de resolver conflictos de versiones.
- Maven utiliza la estrategia de
nearest-wins
, que puede degradar las dependencias transitivas. - Ivy usa la estrategia de las
latest-wins
. - Coursier utiliza principalmente la estrategia de las
latest-wins
, mientras trata de especificar versiones más estrictamente. - El controlador de rango de la versión Ivy va a Internet, lo que hace que la misma compilación no sea repetible.
- Coursier e Ivy organizan representaciones en cadena de versiones de maneras muy diferentes.
Ni siquiera esas sutilezas del ecosistema Scala se discutirán en ScalaConf el 26 de noviembre en Moscú. Artem Seleznev presentará la práctica de trabajar con la base de datos en programación funcional sin JDBC. Wojtek Pitula hablará sobre integración y contará cómo creó una aplicación en la que colocó todas las bibliotecas en funcionamiento. Y se presentarán 16 informes más llenos de hardcore técnico en la conferencia.