依赖性解析工具语义

依赖解决工具


依赖性解析器(以下称为解析器,大约是Transl。)或程序包管理器是一种程序,它考虑到用户设置的限制来定义一组一致的模块。


约束通常由模块名称和版本号指定。 在用于Maven模块的JVM生态系统中,还将显示组织名称(组ID)。 此外,限制可能包括版本范围,排除的模块,版本替代等。


软件包的三个主要类别由OS软件包(自制软件,Debian软件包等)表示,
适用于特定编程语言(CPAN,RubyGem,Maven等)的模块和特定于应用程序的扩展(Eclipse插件,IntelliJ插件,VS Code扩展)。


解析器语义


在第一近似中,我们可以将模块的依赖关系表示为DAG(有向无环图,有向无环图)。


这种表示称为依赖图。 考虑两个模块的依赖关系:


  • a:1.0取决于c:1.0
  • b:1.0取决于c:1.0d:1.0

 +-----+ +-----+ |a:1.0| |b:1.0| +--+--+ +--+--+ | | +<-------+ | | vv +--+--+ +--+--+ |c:1.0| |d:1.0| +-----+ +-----+ 

如果模块依赖于a:1.0b:1.0 ,则将显示完整的依赖项列表a:1.0b:1.0c:1.0d:1.0 。 这只是一个树之旅。


如果通过一系列版本指定传递依赖关系,情况将变得更加复杂:


  • a:1.0取决于c:1.0
  • b:1.0取决于c:[1.0,2)d:1.0

 +-----+ +-----+ |a:1.0| |b:1.0| +--+--+ +--+--+ | | | +-----------+ | | | vvv +--+--+ +--+------+ +--+--+ |c:1.0| |c:[1.0,2)| |d:1.0| +-----+ +---------+ +-----+ 

或者,如果为传递依赖项指定了不同的版本:


  • a:1.0取决于c:1.0
  • b:1.0取决于c:1.2d:1.2

或者,如果对依赖项抛出异常:


  • 依赖于a:1.0 ,取决于c:1.0 ,不包括c:*
  • b:1.0取决于c:1.2d:1.2

不同的解析器对用户设置的限制的解释不同。 我称这类规则为解析器的语义。


您可能需要了解其中一些语义,例如:


  • 您自己模块的语义(由您使用的构建工具确定);
  • 您使用的库的语义(由作者使用的构建工具确定);
  • 模块将用作依赖项的模块的语义(由最终用户构建工具定义)。

JVM生态系统中的依赖关系解决工具


由于我支持sbt ,因此我必须主要在JVM生态系统中工作。


语义专家:最近的胜利


在存在依赖冲突的图中(在依赖图中a有很多不同版本的组件d ,例如d:1.0d:2.0 ),Maven使用最近胜利策略解决冲突。


解决依赖项冲突是一个过程,该过程确定如果在依赖项中找到同一工件的多个不同版本,则将选择工件的哪个版本。 Maven选择最接近的定义。 即 使用在依赖关系树中最接近您的项目的版本。
您始终可以通过在项目的POM中明确声明正确的版本来保证使用正确的版本。 请注意,如果两个版本的依赖关系在树中具有相同的深度,则将选择第一个。 最接近的定义意味着将使用在依赖关系树中最接近项目的版本。 例如,如果将ABC的依赖关系定义为A -> B -> C -> D 2.0A -> E -> D 1.0 ,则在构建A时将使用D 1.0 ,因为 从AD通过E的路径要短(比通过BC的路径要近)。

这意味着使用Maven发布的许多Java模块都是使用nearest-wins语义编译的。 为了说明上述内容,请创建一个简单的pom.xml


 <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返回已解析的classpath
值得注意的是,即使Akka 2.5.3 com.typesafe:config:1.3.1依赖com.typesafe:config:1.3.1 ,生成的树也使用com.typesafe:config:1.2.0


mvn dependency:tree给出了视觉确认:


 [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 

许多库提供向后兼容性,但是除少数例外,不能保证直接兼容,这令人担忧。


Apache Ivy的语义:最新成果


默认情况下,Apache Ivy使用最新胜利策略来解决依赖关系冲突。


如果不存在此容器,则默认冲突管理器用于所有模块。 当前的默认冲突管理器是“最新修订”。
注意:容器conflicts是常春藤文件之一。

SBT 1.3.x版本中SBT 1.3.x内部依赖项解析器是Apache Ivy。 先前使用的pom.xml在SBT中进行了更简短的描述:


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

sbt shell输入show externalDependencyClasspath以获取解析的类路径。 它应该指示com.typesafe:config:1.3.1的版本com.typesafe:config:1.3.1 。 此外,仍会显示以下警告:


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

sbt shell调用evicted可让您获取冲突解决报告:


 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) 

latest-wins语义中,在实践中指定config:1.2.0意味着“为我提供1.2.0或更高版本”。
这种行为比nearest-wins策略更可取,因为传递库的版本未降级。 但是, evicted电话应检查更换是否正确。


语义库里尔:最新胜利


在进行语义描述之前,我将回答一个重要的问题-Coursier的发音。 根据Alex Arshambo的注释,它的发音为“ 小鸡-西”


有趣的是,Coursier的文档中有一个有关版本控制的页面,其中讨论了解决依赖项的语义。


考虑给定间隔的交集:
  • 如果为空(间隔不相交),则存在冲突。
  • 如果未指定间隔,则假定交点由(,)(对应于所有版本的间隔)表示。
    然后,考虑特定版本:
    • 我们丢弃间隔范围以下的特定版本。
    • 如果在间隔的边界上方有特定版本,则存在冲突。
    • 如果特定版本在间隔范围内,则结果应为最新版本。
    • 如果在间隔范围之内或之外没有特定版本,则结果应为间隔。

因为 据说它是 ,因此-这是latest-wins的语义。
您可以通过使用sbt 1.3.0-RC3进行验证。


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

sbt 1.3.0-RC3控制台调用show externalDependencyClasspath将按预期返回com.typesafe:config:1.3.1 。 解决冲突报告报告的内容相同:


 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) 

注意:Apache Ivy模拟了nearest-wins的语义?


从Maven存储库解决模块依赖性时,Ivy会转换POM文件,并将force="true"属性放入高速缓存中的ivy.xml中。


例如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> 

常春藤文档说:


这两个latest冲突管理器考虑了force依赖属性。
因此,直接依赖项可以声明一个force属性(请参见依赖项),指示直接依赖项和间接修订版本,应优先考虑直接依赖项修订版本。

对我而言,这种表述意味着可以使用force="true"来重新定义latest-wins逻辑并模仿nearest-wins语义。 但是,幸运的是,这注定不会发生,我们现在有了latest-wins :如我们所见, sbt 1.2.8选择了com.typesafe:config:1.3.1


但是,当使用严格的冲突管理器时,可以观察到force="true"的效果,该效果似乎已损坏。


 ThisBuild / conflictManager := ConflictManager.strict 

问题是严格的冲突管理器似乎并不能阻止版本替换。 show externalDependencyClasspath高高兴兴地返回com.typesafe:config:1.3.1
一个相关的问题是,添加严格的冲突管理器放入图中的com.typesafe:config:1.3.1版本会导致错误。


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

看起来像这样:


 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]) 

关于版本控制


我们提到了latest-wins语义,这表明字符串表示形式的版本可能以某种顺序出现。
因此,版本控制是语义的一部分。


Apache Ivy中的版本控制过程


这个Javadoc评论说,在创建版本比较器时,Ivy专注于比较PHP版本功能


此函数首先用点替换_,-和+ . 以版本的字符串形式表示并且还添加了. 在不是数字的一切之前和之后。 因此,例如,“ 4.3.2RC1”变为“ 4.3.2.RC.1”。 然后,她从左到右比较收到的零件。

对于包含特殊元素( devalphaabetabRCrc#plp )*的零件,按以下顺序比较元素:

不是特殊元素的任何字符串<dev <alpha = a <beta = b <RC = rc <#<pl = p。

因此,不仅可以比较不同的级别(例如“ 4.1”和“ 4.1.2”),而且可以比较包含特定开发状态信息的特定于PHP的版本。
*约 佩雷夫

我们可以通过编写一个小函数来检查版本的顺序。


 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) 

Coursier中的版本控制程序


有关依赖关系解析语义的GitHub页面上有关于版本控制的部分。


Coursier使用Maven量身定制的版本控制顺序。 在比较之前,版本的字符串表示形式被分解为单独的元素...
为了获得这样的元素,版本之间用字符。,-和_分隔(并且分隔符本身也被丢弃),并用字母数字或数字字母替换。

要编写测试,请创建一个具有依赖项libraryDependencies += "io.get-coursier" %% "coursier-core" % "2.0.0-RC2-6"的子项目libraryDependencies += "io.get-coursier" %% "coursier-core" % "2.0.0-RC2-6"并运行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) 

事实证明,Coursier订购版本号的顺序与Ivy完全不同。
如果您使用了允许的字母标记,那么这种排序可能会引起一些混乱。


关于版本范围


通常,我避免使用版本范围,尽管它们在Maven Central上重新发布的webjar和npm模块中得到了广泛使用。 诸如"is-number": "^4.0.0"可以写在模块"is-number": "^4.0.0" ,它对应于[4.0.0,5)


Apache Ivy中的版本范围处理


在此程序angular-boostrap:0.14.2angular-boostrap:0.14.2取决于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", ) ) 

sbt 1.2.8调用show externalDependencyClasspath将返回angular-bootstrap:0.14.2angular:1.7.81.7.8去了哪里? 当Ivy遇到各种版本时,它实际上会进入Internet并找到它可以找到的内容,有时甚至使用屏幕抓取。


对版本范围的这种处理使程序集不再重复(每几个月运行一次相同的程序集会给您带来不同的结果)。


在Coursier中处理版本范围


github 页面上的Coursier依赖性解析部分
读:


最好定期使用特定版本
如果您的模块依赖于[1.0,2.0)和1.4,将执行版本批准以支持1.4。
如果对1.4有依赖性,则首选此版本[1.0,2.0)。

看起来很有希望。


 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) 

在带有angular-bootstrap:0.14.2的同一程序集上show externalDependencyClasspath ,按angular-bootstrap:0.14.2返回angular-bootstrap:0.14.2angular:1.4.7 。 这是对Ivy的改进。


当使用多个不相交的版本范围时,请考虑更复杂的情况。 例如:


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

sbt 1.3.0-RC3调用show externalDependencyClasspath返回以下错误:


 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) 

从技术上来说是正确的,因为 这些范围不重叠。 而sbt 1.2.8将此解析为is-number:4.0.0


由于版本范围很常见,很烦人,因此我向Coursier提交了“拉取请求”,以实施其他latest-wins语义规则,使您可以从范围的下限选择更高的版本。
参见coursier / coursier#1284


结论


解析器语义根据用户定义的约束定义特定的类路径。


通常,细节上的差异以解决版本冲突的不同方式体现出来。


  • Maven使用nearest-wins策略,可以降低传递依赖项的等级。
  • 常春藤使用latest-wins策略。
  • Coursier主要使用latest-wins策略,同时尝试更严格地指定版本。
  • Ivy版本范围处理程序进入Internet,这使得相同的构建不可重复。
  • Coursier和Ivy以非常不同的方式组织版本的字符串表示形式。

11月26日在莫斯科举行的ScalaConf上,甚至都不会讨论Scala生态系统的这些微妙之处。 Artem Seleznev将介绍在不使用JDBC的函数式编程中使用数据库的实践。 Wojtek Pitula将讨论集成,并介绍他如何创建一个应用程序,并在其中放置所有工作库。 大会上还将发表16篇充满技术核心的报告

Source: https://habr.com/ru/post/zh-CN474106/


All Articles