mirror of https://github.com/sbt/sbt.git
Maven version range improvement
Previously, when the dependency resolver (Ivy) encountered a Maven version range such as `[1.3.0,)` it would go out to the Internet to find the latest version. This would result to a surprising behavior where the eventual version keeps changing over time *even when there's a version of the library that satisfies the range condition*. This changes to some Maven version ranges would be replaced with its lower bound so that when a satisfactory version is found in the dependency graph it will be used. You can disable this behavior using the JVM flag `-Dsbt.modversionrange=false`. Fixes #2954 Ref #2291 / #2953
This commit is contained in:
parent
b6466624a8
commit
54564ba7ce
|
|
@ -1,2 +1,3 @@
|
|||
target/
|
||||
__pycache__
|
||||
tmp/
|
||||
|
|
|
|||
|
|
@ -174,6 +174,17 @@ object CustomPomParser {
|
|||
def isIdentity = false
|
||||
}
|
||||
|
||||
// TODO: It would be better if we can make dd.isForce to `false` when VersionRange.isVersionRange is `true`.
|
||||
private[this] def stripVersionRange(dd: DependencyDescriptor): DependencyDescriptor =
|
||||
VersionRange.stripMavenVersionRange(dd.getDependencyRevisionId.getRevision) match {
|
||||
case Some(newVersion) =>
|
||||
val id = dd.getDependencyRevisionId
|
||||
val newId = ModuleRevisionId.newInstance(id.getOrganisation, id.getName, id.getBranch, newVersion, id.getExtraAttributes)
|
||||
transform(dd, _ => newId)
|
||||
case None => dd
|
||||
}
|
||||
private[sbt] lazy val versionRangeFlag = sys.props.get("sbt.modversionrange") map { _.toLowerCase == "true" } getOrElse true
|
||||
|
||||
import collection.JavaConverters._
|
||||
def addExtra(properties: Map[String, String], dependencyExtra: Map[ModuleRevisionId, Map[String, String]], parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor =
|
||||
{
|
||||
|
|
@ -199,7 +210,10 @@ object CustomPomParser {
|
|||
IvySbt.addExtraNamespace(dmd)
|
||||
|
||||
val withExtra = md.getDependencies map { dd => addExtra(dd, dependencyExtra) }
|
||||
val unique = IvySbt.mergeDuplicateDefinitions(withExtra)
|
||||
val withVersionRangeMod: Seq[DependencyDescriptor] =
|
||||
if (versionRangeFlag) withExtra map { stripVersionRange }
|
||||
else withExtra
|
||||
val unique = IvySbt.mergeDuplicateDefinitions(withVersionRangeMod)
|
||||
unique foreach dmd.addDependency
|
||||
|
||||
for (ed <- md.getInheritedDescriptors) dmd.addInheritedDescriptor(new DefaultExtendsDescriptor(md, ed.getLocation, ed.getExtendsTypes))
|
||||
|
|
|
|||
|
|
@ -23,56 +23,10 @@ import org.apache.ivy.plugins.resolver.{ ChainResolver, DependencyResolver, IBib
|
|||
import ivyint.CustomRemoteMavenResolver
|
||||
object MakePom {
|
||||
/** True if the revision is an ivy-range, not a complete revision. */
|
||||
def isDependencyVersionRange(revision: String): Boolean = {
|
||||
(revision endsWith "+") ||
|
||||
(revision contains "[") ||
|
||||
(revision contains "]") ||
|
||||
(revision contains "(") ||
|
||||
(revision contains ")")
|
||||
}
|
||||
def isDependencyVersionRange(revision: String): Boolean = VersionRange.isVersionRange(revision)
|
||||
|
||||
/** Converts Ivy revision ranges to that of Maven POM */
|
||||
def makeDependencyVersion(revision: String): String = {
|
||||
def plusRange(s: String, shift: Int = 0) = {
|
||||
def pow(i: Int): Int = if (i > 0) 10 * pow(i - 1) else 1
|
||||
val (prefixVersion, lastVersion) = (s + "0" * shift).reverse.split("\\.", 2) match {
|
||||
case Array(revLast, revRest) =>
|
||||
(revRest.reverse + ".", revLast.reverse)
|
||||
case Array(revLast) => ("", revLast.reverse)
|
||||
}
|
||||
val lastVersionInt = lastVersion.toInt
|
||||
s"[${prefixVersion}${lastVersion},${prefixVersion}${lastVersionInt + pow(shift)})"
|
||||
}
|
||||
val startSym = Set(']', '[', '(')
|
||||
val stopSym = Set(']', '[', ')')
|
||||
val DotPlusPattern = """(.+)\.\+""".r
|
||||
val DotNumPlusPattern = """(.+)\.(\d+)\+""".r
|
||||
val NumPlusPattern = """(\d+)\+""".r
|
||||
val maxDigit = 5
|
||||
try {
|
||||
revision match {
|
||||
case "+" => "[0,)"
|
||||
case DotPlusPattern(base) => plusRange(base)
|
||||
// This is a heuristic. Maven just doesn't support Ivy's notions of 1+, so
|
||||
// we assume version ranges never go beyond 5 siginificant digits.
|
||||
case NumPlusPattern(tail) => (0 until maxDigit).map(plusRange(tail, _)).mkString(",")
|
||||
case DotNumPlusPattern(base, tail) => (0 until maxDigit).map(plusRange(base + "." + tail, _)).mkString(",")
|
||||
case rev if rev endsWith "+" => sys.error(s"dynamic revision '$rev' cannot be translated to POM")
|
||||
case rev if startSym(rev(0)) && stopSym(rev(rev.length - 1)) =>
|
||||
val start = rev(0)
|
||||
val stop = rev(rev.length - 1)
|
||||
val mid = rev.substring(1, rev.length - 1)
|
||||
(if (start == ']') "(" else start) + mid + (if (stop == '[') ")" else stop)
|
||||
case _ => revision
|
||||
}
|
||||
} catch {
|
||||
case e: NumberFormatException =>
|
||||
// TODO - if the version doesn't meet our expectations, maybe we just issue a hard
|
||||
// error instead of softly ignoring the attempt to rewrite.
|
||||
//sys.error(s"Could not fix version [$revision] into maven style version")
|
||||
revision
|
||||
}
|
||||
}
|
||||
def makeDependencyVersion(revision: String): String = VersionRange.fromIvyToMavenVersion(revision)
|
||||
}
|
||||
class MakePom(val log: Logger) {
|
||||
import MakePom._
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package sbt
|
||||
|
||||
object VersionRange {
|
||||
/** True if the revision is an ivy-range, not a complete revision. */
|
||||
def isVersionRange(revision: String): Boolean = {
|
||||
(revision endsWith "+") ||
|
||||
(revision contains "[") ||
|
||||
(revision contains "]") ||
|
||||
(revision contains "(") ||
|
||||
(revision contains ")")
|
||||
}
|
||||
|
||||
// Assuming Ivy is used to resolve conflict, this removes the version range
|
||||
// when it is open-ended to avoid dependency resolution hitting the Internet to get the latest.
|
||||
// See https://github.com/sbt/sbt/issues/2954
|
||||
def stripMavenVersionRange(version: String): Option[String] =
|
||||
if (isVersionRange(version)) {
|
||||
val noSpace = version.replaceAllLiterally(" ", "")
|
||||
noSpace match {
|
||||
case MavenVersionSetPattern(open1, x1, comma, x2, close1, rest) =>
|
||||
// http://maven.apache.org/components/enforcer/enforcer-rules/versionRanges.html
|
||||
(open1, Option(x1), Option(comma), Option(x2), close1) match {
|
||||
case (_, None, _, Some(x2), "]") => Some(x2)
|
||||
// a good upper bound is unknown
|
||||
case (_, None, _, Some(x2), ")") => None
|
||||
case (_, Some(x1), _, None, _) => Some(x1)
|
||||
case _ => None
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
} else None
|
||||
|
||||
/** Converts Ivy revision ranges to that of Maven POM */
|
||||
def fromIvyToMavenVersion(revision: String): String = {
|
||||
def plusRange(s: String, shift: Int = 0) = {
|
||||
def pow(i: Int): Int = if (i > 0) 10 * pow(i - 1) else 1
|
||||
val (prefixVersion, lastVersion) = (s + "0" * shift).reverse.split("\\.", 2) match {
|
||||
case Array(revLast, revRest) =>
|
||||
(revRest.reverse + ".", revLast.reverse)
|
||||
case Array(revLast) => ("", revLast.reverse)
|
||||
}
|
||||
val lastVersionInt = lastVersion.toInt
|
||||
s"[${prefixVersion}${lastVersion},${prefixVersion}${lastVersionInt + pow(shift)})"
|
||||
}
|
||||
val DotPlusPattern = """(.+)\.\+""".r
|
||||
val DotNumPlusPattern = """(.+)\.(\d+)\+""".r
|
||||
val NumPlusPattern = """(\d+)\+""".r
|
||||
val maxDigit = 5
|
||||
try {
|
||||
revision match {
|
||||
case "+" => "[0,)"
|
||||
case DotPlusPattern(base) => plusRange(base)
|
||||
// This is a heuristic. Maven just doesn't support Ivy's notions of 1+, so
|
||||
// we assume version ranges never go beyond 5 siginificant digits.
|
||||
case NumPlusPattern(tail) => (0 until maxDigit).map(plusRange(tail, _)).mkString(",")
|
||||
case DotNumPlusPattern(base, tail) => (0 until maxDigit).map(plusRange(base + "." + tail, _)).mkString(",")
|
||||
case rev if rev endsWith "+" => sys.error(s"dynamic revision '$rev' cannot be translated to POM")
|
||||
case rev if startSym(rev(0)) && stopSym(rev(rev.length - 1)) =>
|
||||
val start = rev(0)
|
||||
val stop = rev(rev.length - 1)
|
||||
val mid = rev.substring(1, rev.length - 1)
|
||||
(if (start == ']') "(" else start) + mid + (if (stop == '[') ")" else stop)
|
||||
case _ => revision
|
||||
}
|
||||
} catch {
|
||||
case e: NumberFormatException =>
|
||||
// TODO - if the version doesn't meet our expectations, maybe we just issue a hard
|
||||
// error instead of softly ignoring the attempt to rewrite.
|
||||
//sys.error(s"Could not fix version [$revision] into maven style version")
|
||||
revision
|
||||
}
|
||||
}
|
||||
|
||||
def hasMavenVersionRange(version: String): Boolean =
|
||||
if (version.length <= 1) false
|
||||
else startSym(version(0)) && stopSym(version(version.length - 1))
|
||||
|
||||
private[this] val startSym = Set(']', '[', '(')
|
||||
private[this] val stopSym = Set(']', '[', ')')
|
||||
private[this] val MavenVersionSetPattern = """([\]\[\(])([\w\.\-]+)?(,)?([\w\.\-]+)?([\]\[\)])(,.+)?""".r
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package sbt
|
||||
|
||||
import org.specs2._
|
||||
|
||||
class VersionRangeSpec extends Specification {
|
||||
def is = s2"""
|
||||
|
||||
This is a specification to check the version range parsing.
|
||||
|
||||
1.0 should
|
||||
${stripTo("1.0", None)}
|
||||
(,1.0] should
|
||||
${stripTo("(,1.0]", Some("1.0"))}
|
||||
(,1.0) should
|
||||
${stripTo("(,1.0)", None)}
|
||||
[1.0] should
|
||||
${stripTo("[1.0]", Some("1.0"))}
|
||||
[1.0,) should
|
||||
${stripTo("[1.0,)", Some("1.0"))}
|
||||
(1.0,) should
|
||||
${stripTo("(1.0,)", Some("1.0"))}
|
||||
(1.0,2.0) should
|
||||
${stripTo("(1.0,2.0)", None)}
|
||||
[1.0,2.0] should
|
||||
${stripTo("[1.0,2.0]", None)}
|
||||
(,1.0],[1.2,) should
|
||||
${stripTo("(,1.0],[1.2,)", Some("1.0"))}
|
||||
(,1.1),(1.1,) should
|
||||
${stripTo("(,1.1),(1.1,)", None)}
|
||||
"""
|
||||
|
||||
def stripTo(s: String, expected: Option[String]) =
|
||||
VersionRange.stripMavenVersionRange(s) must_== expected
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
### Fixes with compatibility implications
|
||||
|
||||
- sbt 0.13.14 removes the Maven version range when possible. See below.
|
||||
|
||||
### Maven version range improvement
|
||||
|
||||
Previously, when the dependency resolver (Ivy) encountered a Maven version range such as `[1.3.0,)`
|
||||
it would go out to the Internet to find the latest version.
|
||||
This would result to a surprising behavior where the eventual version keeps changing over time
|
||||
*even when there's a version of the library that satisfies the range condition*.
|
||||
|
||||
Starting sbt 0.13.14, some Maven version ranges would be replaced with its lower bound
|
||||
so that when a satisfactory version is found in the dependency graph it will be used.
|
||||
You can disable this behavior using the JVM flag `-Dsbt.modversionrange=false`.
|
||||
|
||||
[#2954][2954] by [@eed3si9n][@eed3si9n]
|
||||
|
||||
[@eed3si9n]: https://github.com/eed3si9n
|
||||
[@dwijnand]: https://github.com/dwijnand
|
||||
[@Duhemm]: https://github.com/Duhemm
|
||||
[2954]: https://github.com/sbt/sbt/issues/2954
|
||||
|
|
@ -1,5 +1,23 @@
|
|||
lazy val check = taskKey[Unit]("Runs the check")
|
||||
|
||||
def commonSettings: Seq[Def.Setting[_]] =
|
||||
Seq(
|
||||
ivyPaths := new IvyPaths( (baseDirectory in ThisBuild).value, Some((target in LocalRootProject).value / "ivy-cache")),
|
||||
scalaVersion := "2.10.6"
|
||||
)
|
||||
|
||||
lazy val root = (project in file(".")).
|
||||
settings(
|
||||
libraryDependencies += "org.webjars" %% "webjars-play" % "2.1.0-3",
|
||||
resolvers += Resolver.typesafeRepo("releases")
|
||||
commonSettings,
|
||||
libraryDependencies ++= Seq(
|
||||
"org.webjars.bower" % "angular" % "1.4.7",
|
||||
"org.webjars.bower" % "angular-bootstrap" % "0.14.2"
|
||||
),
|
||||
resolvers += Resolver.typesafeRepo("releases"),
|
||||
check := {
|
||||
val acp = (externalDependencyClasspath in Compile).value.map {_.data.getName}.sorted
|
||||
if (!(acp contains "angular-1.4.7.jar")) {
|
||||
sys.error("angular-1.4.7.jar not found when it should be included: " + acp.toString)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
> update
|
||||
> check
|
||||
|
|
|
|||
Loading…
Reference in New Issue