依赖解决工具
依赖性解析器(以下称为解析器,大约是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.0
和d:1.0
+-----+ +-----+ |a:1.0| |b:1.0| +--+--+ +--+--+ | | +<-------+ | | vv +--+--+ +--+--+ |c:1.0| |d:1.0| +-----+ +-----+
如果模块依赖于a:1.0
和b:1.0
,则将显示完整的依赖项列表a:1.0
, b:1.0
, c:1.0
和d: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.2
和d:1.2
或者,如果对依赖项抛出异常:
- 依赖于
a:1.0
,取决于c:1.0
,不包括c:*
b:1.0
取决于c:1.2
和d:1.2
不同的解析器对用户设置的限制的解释不同。 我称这类规则为解析器的语义。
您可能需要了解其中一些语义,例如:
- 您自己模块的语义(由您使用的构建工具确定);
- 您使用的库的语义(由作者使用的构建工具确定);
- 模块将用作依赖项的模块的语义(由最终用户构建工具定义)。
JVM生态系统中的依赖关系解决工具
由于我支持sbt
,因此我必须主要在JVM生态系统中工作。
语义专家:最近的胜利
在存在依赖冲突的图中(在依赖图中a
有很多不同版本的组件d
,例如d:1.0
和d:2.0
),Maven使用最近胜利策略解决冲突。
解决依赖项冲突是一个过程,该过程确定如果在依赖项中找到同一工件的多个不同版本,则将选择工件的哪个版本。 Maven选择最接近的定义。 即 使用在依赖关系树中最接近您的项目的版本。
您始终可以通过在项目的POM中明确声明正确的版本来保证使用正确的版本。 请注意,如果两个版本的依赖关系在树中具有相同的深度,则将选择第一个。 最接近的定义意味着将使用在依赖关系树中最接近项目的版本。 例如,如果将A
, B
和C
的依赖关系定义为A -> B -> C -> D 2.0
和A -> E -> D 1.0
,则在构建A
时将使用D 1.0
,因为 从A
到D
通过E
的路径要短(比通过B
和C
的路径要近)。
这意味着使用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”。 然后,她从左到右比较收到的零件。
对于包含特殊元素( dev
, alpha
或a
, beta
或b
, RC
或rc
, #
, pl
或p
)*的零件,按以下顺序比较元素:
不是特殊元素的任何字符串<dev <alpha = a <beta = b <RC = rc <#<pl = p。
因此,不仅可以比较不同的级别(例如“ 4.1”和“ 4.1.2”),而且可以比较包含特定开发状态信息的特定于PHP的版本。
*约 佩雷夫
我们可以通过编写一个小函数来检查版本的顺序。
scala> :paste
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.2
, angular-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.2
和angular:1.7.8
。 1.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.2
和angular: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篇充满技术核心的报告 。