Semântica das Ferramentas de Resolução de Dependências

Ferramenta de Resolução de Dependências


Um resolvedor de dependências (daqui em diante referido como resolvedor, aprox. Transl.) Ou um gerenciador de pacotes é um programa que define um conjunto consistente de módulos, levando em consideração as restrições definidas pelo usuário.


As restrições são geralmente especificadas por nomes de módulos e números de versão. No ecossistema da JVM para os módulos Maven, o nome da organização (ID do grupo) também será indicado. Além disso, as restrições podem incluir intervalos de versões, módulos excluídos, substituições de versões etc.


As três principais categorias de pacotes são representadas por pacotes de SO (Homebrew, pacotes Debian, etc.),
módulos para linguagens de programação específicas (CPAN, RubyGem, Maven, etc) e extensões específicas de aplicativos (plugins Eclipse, plugins IntelliJ, extensões VS Code).


Semântica do resolvedor


Numa primeira aproximação, podemos representar as dependências dos módulos como um DAG (gráfico acíclico direcionado, gráfico acíclico direcionado).


Essa representação é chamada de gráfico de dependência. Considere as dependências de dois 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| +-----+ +-----+ 

Se o módulo depender de a:1.0 b:1.0 , será apresentada a:1.0 lista completa de dependências a:1.0 , b:1.0 , c:1.0 d:1.0 . E este é apenas um passeio pelas árvores.


A situação ficará mais complicada se as dependências transitivas forem especificadas por um intervalo de versões:


  • 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| +-----+ +---------+ +-----+ 

Ou, se versões diferentes forem especificadas para dependências transitivas:


  • a:1.0 depende de c:1.0
  • b:1.0 depende de c:1.2 d:1.2

Ou, se exceções forem lançadas para a dependência:


  • dependência de a:1.0 , que depende de c:1.0 , excluindo c:*
  • b:1.0 depende de c:1.2 d:1.2

Resolvedores diferentes interpretam as restrições definidas pelos usuários de maneira diferente. Eu chamo essas regras de semântica dos resolvedores.


Você pode precisar conhecer algumas dessas semânticas, por exemplo:


  • semântica do seu próprio módulo (determinada pela ferramenta de construção que você usa);
  • a semântica das bibliotecas que você usa (determinada pela ferramenta de construção que o autor usou);
  • a semântica dos módulos que seu módulo usará como uma dependência (definida pela ferramenta de criação do usuário final).

Ferramentas de resolução de dependência no ecossistema da JVM


Como eu apóio o sbt , tenho que trabalhar principalmente no ecossistema da JVM.


Semantics Maven: vitórias mais próximas


Nos gráficos em que há um conflito de dependências (no gráfico de dependência a existem muitas versões diferentes do componente d , por exemplo, d:1.0 d:2.0 ), o Maven usa a estratégia de ganhos mais próximos para resolver o conflito.


A resolução de conflitos de dependência é um processo que determina qual versão de um artefato será selecionada se várias versões diferentes do mesmo artefato forem encontradas entre as dependências. Maven seleciona a definição mais próxima. I.e. usa a versão mais próxima do seu projeto na árvore de dependências.
Você sempre pode garantir o uso da versão correta declarando-a explicitamente no POM do projeto. Observe que, se duas versões da dependência tiverem a mesma profundidade na árvore, a primeira será selecionada. A definição mais próxima significa que a versão mais próxima do projeto na árvore de dependência será usada. Por exemplo, se as dependências para A , B e C definidas como A -> B -> C -> D 2.0 e A -> E -> D 1.0 , então, ao criar A , D 1.0 será usado, porque o caminho de A a D via E mais curto (do que via B e C , aprox. transl.).

Isso significa que muitos módulos Java publicados usando o Maven foram compilados usando a semântica dos nearest-wins . Para ilustrar o acima, crie um pom.xml simples:


 <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 retorna o classpath resolvido.
Vale ressaltar que a árvore resultante usa com.typesafe:config:1.2.0 mesmo que o Akka 2.5.3 transitivamente dependente de com.typesafe:config:1.3.1 .


mvn dependency:tree dá essa confirmação 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 

Muitas bibliotecas fornecem compatibilidade com versões anteriores, mas a compatibilidade direta não é garantida com algumas exceções, o que é alarmante.


Semântica do Apache Ivy: últimas vitórias


Por padrão, o Apache Ivy usa a estratégia mais recente para resolver conflitos de dependência.


Se esse contêiner não estiver presente, o gerenciador de conflitos padrão será usado para todos os módulos. O gerenciador de conflitos padrão atual é "revisão mais recente".
Nota: Os conflicts contêiner são um dos arquivos Ivy.

Até a versão SBT 1.3.x resolvedor de dependência interno é o Apache Ivy. O pom.xml usado anteriormente é descrito no SBT um pouco mais 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", ) 

No sbt shell digite show externalDependencyClasspath para obter o caminho de classe resolvido. Deve indicar a versão do com.typesafe:config:1.3.1 . Além disso, o seguinte aviso ainda será exibido:


 [warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings. 

Chamar o evicted no sbt shell permite obter um relatório de resolução de conflitos:


 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) 

Na semântica das latest-wins , especificar config:1.2.0 na prática significa "fornecer-me a versão 1.2.0 ou superior".
Esse comportamento é um pouco mais preferível do que na estratégia de nearest-wins , porque as versões das bibliotecas transitivas não são rebaixadas. No entanto, a chamada evicted deve verificar se as substituições foram feitas corretamente.


Semantics Coursier: últimas vitórias


Antes de abordarmos a descrição da semântica, responderei a uma pergunta importante - como é pronunciado Coursier. De acordo com a nota de Alex Arshambo , é pronunciado chick-sie .


Curiosamente, a documentação do Coursier tem uma página sobre controle de versão , que fala sobre a semântica da resolução de dependências.


Considere a interseção de determinados intervalos:
  • Se estiver vazio (os intervalos não se cruzam), há um conflito.
  • Se nenhum intervalo for especificado, presume-se que a interseção seja representada por (,) (o intervalo correspondente a todas as versões).
    Em seguida, considere versões específicas:
    • Descartamos versões específicas abaixo dos limites do intervalo.
    • Se houver versões específicas acima dos limites do intervalo, haverá um conflito.
    • Se versões específicas estiverem dentro dos limites do intervalo, o resultado deverá ser o mais recente deles.
    • Se não houver versões específicas dentro ou acima dos limites do intervalo, o resultado deverá ser o intervalo.

Porque diz-se , portanto - esta é a semântica das latest-wins .
Você pode verificar isso usando o sbt 1.3.0-RC3 , que usa o 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", ) 

Chamar show externalDependencyClasspath partir do console do sbt 1.3.0-RC3 retornará com.typesafe:config:1.3.1 , conforme o esperado. O Relatório de resolução de conflitos informa o mesmo:


 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: O Apache Ivy emula a semântica das nearest-wins ?


Ao resolver dependências do módulo no repositório Maven, o Ivy converte o arquivo POM e coloca o atributo force="true" em ivy.xml no cache.


Por exemplo, 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> 

A documentação do Ivy diz:


Esses dois latest gerenciadores de conflitos levam em consideração o atributo de dependência de force .
Assim, dependências diretas podem declarar um atributo de force (consulte dependência), indicando que, de dependência direta e revisões indiretas, deve-se dar preferência a revisões diretas de dependência.

Para mim, essa formulação significa que force="true" concebido para redefinir a lógica das latest-wins e emular a semântica das nearest-wins . Mas, felizmente, isso não estava destinado a acontecer, e agora temos as latest-wins : como podemos ver, o sbt 1.2.8 pega com.typesafe:config:1.3.1 .


No entanto, pode-se observar o efeito de force="true" ao usar um gerenciador de conflitos estrito, que parece estar quebrado.


 ThisBuild / conflictManager := ConflictManager.strict 

O problema é que um gerenciador de conflitos estrito não parece impedir a substituição da versão. show externalDependencyClasspath alegremente retorna com.typesafe:config:1.3.1 .
Um problema relacionado é que a adição de uma versão do com.typesafe:config:1.3.1 , que um gerente de conflitos rigoroso coloca no gráfico, leva a um erro.


 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", ) ) 

É assim:


 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 controle de versão


Mencionamos a semântica das latest-wins , sugerindo que as versões em uma representação de string podem ocorrer em alguma ordem.
Portanto, o controle de versão faz parte da semântica.


Procedimento de versão no Apache Ivy


Este comentário do Javadoc diz que, ao criar o comparador de versões, Ivy se concentrou na função de comparar versões do PHP :


Esta função primeiro substitui _, - e + por um ponto . em representações de string de versões e também adiciona . antes e depois de tudo que não é um número. Assim, por exemplo, '4.3.2RC1' se torna '4.3.2.RC.1'. Ela então compara as partes recebidas da esquerda para a direita.

Para peças que contêm elementos especiais ( dev , alpha ou a , beta ou b , RC ou rc , # , pl ou p ) *, os elementos são comparados na seguinte ordem:

qualquer sequência que não seja um elemento especial <dev <alfa = a <beta = b <RC = rc <# <pl = p.

Portanto, não apenas níveis diferentes (por exemplo, '4.1' e '4.1.2') podem ser comparados, mas versões específicas do PHP contendo informações sobre o status do desenvolvimento.
* aprox. perev.

Podemos verificar como as versões são ordenadas escrevendo uma pequena função.


 scala> :paste // Entering paste mode (ctrl-D to finish) val strategy = new org.apache.ivy.plugins.latest.LatestRevisionStrategy case class MockArtifactInfo(version: String) extends org.apache.ivy.plugins.latest.ArtifactInfo { def getRevision: String = version def getLastModified: Long = -1 } def sortVersionsIvy(versions: String*): List[String] = { import scala.collection.JavaConverters._ strategy.sort(versions.toArray map MockArtifactInfo) .asScala.toList map { case MockArtifactInfo(v) => v } } // Exiting paste mode, now interpreting. scala> sortVersionsIvy("1.0", "2.0", "1.0-alpha", "1.0+alpha", "1.0-X1", "1.0a", "2.0.2") res7: List[String] = List(1.0-X1, 1.0a, 1.0-alpha, 1.0+alpha, 1.0, 2.0, 2.0.2) 

Procedimento de controle de versão no Coursier


A página do GitHub sobre semântica de resolução de dependências possui uma seção sobre controle de versão.


O Coursier usa a ordem de versão personalizada do Maven. Antes de comparar, as representações de string das versões são divididas em elementos separados ...
Para obter esses elementos, as versões são separadas pelos caracteres., - e _ (e os próprios separadores são descartados) e por substituições de letra a número ou número a letra.

Para escrever um teste, crie um subprojeto com as libraryDependencies += "io.get-coursier" %% "coursier-core" % "2.0.0-RC2-6" e execute o 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 se vê, Coursier ordena números de versão em uma ordem completamente diferente da Ivy.
Se você usou tags alfabéticas permissivas, essa ordem pode causar alguma confusão.


Sobre os intervalos de versão


Normalmente, evito usar intervalos de versões, embora sejam amplamente usados ​​em webjars e módulos npm republicados no Maven Central. Algo como "is-number": "^4.0.0" pode ser escrito no módulo "is-number": "^4.0.0" que corresponderá a [4.0.0,5) .


Manipulação do intervalo de versões no Apache Ivy


Nesta montagem, 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", ) ) 

Chamar show externalDependencyClasspath no sbt 1.2.8 retornará angular-bootstrap:0.14.2 e angular:1.7.8 . E para onde 1.7.8 foi? Quando o Ivy encontra uma variedade de versões, ele essencialmente acessa a Internet e encontra o que pode encontrar, às vezes até usando captura de tela.


Esse processamento de intervalos de versão torna as montagens não repetitivas (a execução da mesma montagem a cada poucos meses fornece resultados diferentes).


Manuseio de versões em Coursier


Seção de resolução de dependência do Coursier na página do github
lê:


Versões específicas em intervalos são preferidas
Se o seu módulo depende de [1.0,2.0) e 1.4, a aprovação da versão será realizada em favor do 1.4.
Se houver uma dependência do 1.4, essa versão será preferida no intervalo [1.0,2.0).

Parece promissor.


 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 no mesmo assembly com angular-bootstrap:0.14.2 retorna angular-bootstrap:0.14.2 e angular:1.4.7 conforme o esperado. Esta é uma melhoria em relação à Ivy.


Considere o caso mais complicado quando vários intervalos de versão separados são usados. Por exemplo:


 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", ) ) 

Chamar show externalDependencyClasspath no sbt 1.3.0-RC3 retorna o seguinte erro:


 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) 

Tecnicamente, isso mesmo, porque esses intervalos não se sobrepõem. Enquanto o sbt 1.2.8 resolve isso como is-number:4.0.0 .


Devido ao fato de os intervalos de versão serem comuns o suficiente para serem irritantes, eu envio uma solicitação de solicitação ao Coursier para implementar regras semânticas adicionais das latest-wins que permitem selecionar versões posteriores dos limites inferiores dos intervalos.
Ver coursier / coursier # 1284 .


Conclusão


A semântica do resolvedor define um caminho de classe específico com base nas restrições definidas pelo usuário.


Geralmente, as diferenças nos detalhes são manifestadas de diferentes maneiras para resolver conflitos de versão.


  • O Maven usa a estratégia de nearest-wins , que pode rebaixar dependências transitivas.
  • Ivy usa a estratégia de latest-wins .
  • O Coursier usa principalmente a estratégia de latest-wins , enquanto tenta especificar versões mais estritamente.
  • O manipulador de intervalo da versão Ivy vai para a Internet, o que torna a mesma compilação não repetível.
  • Coursier e Ivy organizam representações de strings de versões de maneiras muito diferentes.

Nem mesmo essas sutilezas do ecossistema Scala serão discutidas na ScalaConf em 26 de novembro em Moscou. Artem Seleznev apresentará a prática de trabalhar com o banco de dados em programação funcional sem JDBC. Wojtek Pitula falará sobre integração e contará como ele criou um aplicativo no qual colocou todas as bibliotecas de trabalho. E mais 16 relatórios cheios de hardcore técnico serão apresentados na conferência.

Source: https://habr.com/ru/post/pt474106/


All Articles