summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillaume Galy <guilgaly@users.noreply.github.com>2018-07-14 03:20:19 +0200
committerLi Haoyi <haoyi.sg@gmail.com>2018-07-14 09:20:19 +0800
commit5616dc9b46e033b39d0df12e42173ee1c875cee4 (patch)
treef289a8f06bf1057479e88cb0e5b66d642bc32b91
parent2c5546d67789e774610bad28b710e357f37fc0d2 (diff)
downloadmill-5616dc9b46e033b39d0df12e42173ee1c875cee4.tar.gz
mill-5616dc9b46e033b39d0df12e42173ee1c875cee4.tar.bz2
mill-5616dc9b46e033b39d0df12e42173ee1c875cee4.zip
Issue #314; port sbt-updates to mill (#340)
* Implement basic dependency resolution * Implement basic dependency versions resolution (Maven only) * refactor dependency updates code * add resolution of updated dependencies * remove dependency on locally-built coursier * dependency updates output formatting * Add 'allowPreRelease' option * start adding tests * Add more tests * Add documentation * Cleanup code * rewrite version parser to use fastparse
-rw-r--r--docs/pages/1 - Intro to Mill.md23
-rw-r--r--scalalib/src/mill/scalalib/Dependency.scala22
-rw-r--r--scalalib/src/mill/scalalib/dependency/DependencyUpdatesImpl.scala52
-rw-r--r--scalalib/src/mill/scalalib/dependency/metadata/MavenMetadataLoader.scala19
-rw-r--r--scalalib/src/mill/scalalib/dependency/metadata/MetadataLoader.scala7
-rw-r--r--scalalib/src/mill/scalalib/dependency/metadata/MetadataLoaderFactory.scala11
-rw-r--r--scalalib/src/mill/scalalib/dependency/updates/ModuleDependenciesUpdates.scala15
-rw-r--r--scalalib/src/mill/scalalib/dependency/updates/UpdatesFinder.scala75
-rw-r--r--scalalib/src/mill/scalalib/dependency/versions/ModuleDependenciesVersions.scala12
-rw-r--r--scalalib/src/mill/scalalib/dependency/versions/Version.scala227
-rw-r--r--scalalib/src/mill/scalalib/dependency/versions/VersionParser.scala30
-rw-r--r--scalalib/src/mill/scalalib/dependency/versions/VersionsFinder.scala74
-rw-r--r--scalalib/test/src/mill/scalalib/dependency/metadata/MetadataLoaderFactoryTests.scala64
-rw-r--r--scalalib/test/src/mill/scalalib/dependency/updates/UpdatesFinderTests.scala173
-rw-r--r--scalalib/test/src/mill/scalalib/dependency/versions/VersionTests.scala139
15 files changed, 943 insertions, 0 deletions
diff --git a/docs/pages/1 - Intro to Mill.md b/docs/pages/1 - Intro to Mill.md
index bbed0ed7..439e80e5 100644
--- a/docs/pages/1 - Intro to Mill.md
+++ b/docs/pages/1 - Intro to Mill.md
@@ -566,6 +566,29 @@ mill clean _.compile
mill clean __.compile
```
+### Search for dependency updates
+
+```bash
+$ mill mill.scalalib.Dependency/updates
+```
+
+Mill can search for updated versions of your project's dependencies,
+if available from your project's configured repositories. Note that it
+uses heuristics based on common versionning schemes, so it may not work
+as expected for dependencies with particularly weird version numbers.
+
+Current limitations:
+- Only works for `JavaModule`s (including `ScalaModule`s,
+`CrossScalaModule`s, etc.) and Maven repositories.
+- Always applies to all modules in the build.
+- Doesn't apply to `$ivy` dependencies used in the build definition
+itself.
+
+```bash
+mill mill.scalalib.Dependency/updates
+mill mill.scalalib.Dependency/updates --allowPreRelease true # also show pre-release versions
+```
+
## IntelliJ Support
Mill supports IntelliJ by default. Use `mill mill.scalalib.GenIdea/idea` to
diff --git a/scalalib/src/mill/scalalib/Dependency.scala b/scalalib/src/mill/scalalib/Dependency.scala
new file mode 100644
index 00000000..858e479b
--- /dev/null
+++ b/scalalib/src/mill/scalalib/Dependency.scala
@@ -0,0 +1,22 @@
+package mill.scalalib
+
+import mill.T
+import mill.define.{Discover, ExternalModule}
+import mill.eval.Evaluator
+import mill.main.EvaluatorScopt
+import mill.scalalib.dependency.DependencyUpdatesImpl
+
+object Dependency extends ExternalModule {
+
+ def updates(ev: Evaluator[Any], allowPreRelease: Boolean = false) =
+ T.command {
+ DependencyUpdatesImpl(implicitly,
+ ev.rootModule,
+ ev.rootModule.millDiscover,
+ allowPreRelease)
+ }
+
+ implicit def millScoptEvaluatorReads[T]: EvaluatorScopt[T] =
+ new mill.main.EvaluatorScopt[T]()
+ lazy val millDiscover: Discover[Dependency.this.type] = Discover[this.type]
+}
diff --git a/scalalib/src/mill/scalalib/dependency/DependencyUpdatesImpl.scala b/scalalib/src/mill/scalalib/dependency/DependencyUpdatesImpl.scala
new file mode 100644
index 00000000..dc548c88
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/DependencyUpdatesImpl.scala
@@ -0,0 +1,52 @@
+package mill.scalalib.dependency
+
+import mill.define._
+import mill.scalalib.dependency.updates.{
+ DependencyUpdates,
+ ModuleDependenciesUpdates,
+ UpdatesFinder
+}
+import mill.scalalib.dependency.versions.VersionsFinder
+import mill.util.Ctx.{Home, Log}
+
+object DependencyUpdatesImpl {
+
+ def apply(ctx: Log with Home,
+ rootModule: BaseModule,
+ discover: Discover[_],
+ allowPreRelease: Boolean): Unit = {
+
+ // 1. Find all available versions for each dependency
+ val allDependencyVersions = VersionsFinder.findVersions(ctx, rootModule)
+
+ // 2. Extract updated versions from all available versions
+ val allUpdates = allDependencyVersions.map { dependencyVersions =>
+ UpdatesFinder.findUpdates(dependencyVersions, allowPreRelease)
+ }
+
+ // 3. Print the results
+ showAllUpdates(allUpdates)
+ }
+
+ private def showAllUpdates(updates: Seq[ModuleDependenciesUpdates]): Unit =
+ updates.foreach { dependencyUpdates =>
+ val module = dependencyUpdates.module.toString
+ val actualUpdates =
+ dependencyUpdates.dependencies.filter(_.updates.nonEmpty)
+ if (actualUpdates.isEmpty) {
+ println(s"No dependency updates found for $module")
+ } else {
+ println(s"Found ${actualUpdates.length} dependency update for $module")
+ showUpdates(actualUpdates)
+ }
+ }
+
+ private def showUpdates(updates: Seq[DependencyUpdates]): Unit =
+ updates.foreach { dependencyUpdate =>
+ val module = s"${dependencyUpdate.dependency.module}"
+ val allVersions =
+ (dependencyUpdate.currentVersion +: dependencyUpdate.updates.toList)
+ .mkString(" -> ")
+ println(s" $module : $allVersions")
+ }
+}
diff --git a/scalalib/src/mill/scalalib/dependency/metadata/MavenMetadataLoader.scala b/scalalib/src/mill/scalalib/dependency/metadata/MavenMetadataLoader.scala
new file mode 100644
index 00000000..7f25764b
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/metadata/MavenMetadataLoader.scala
@@ -0,0 +1,19 @@
+package mill.scalalib.dependency.metadata
+
+import coursier.Cache
+import coursier.maven.MavenRepository
+import mill.scalalib.dependency.versions.Version
+
+private[dependency] final case class MavenMetadataLoader(mavenRepo: MavenRepository)
+ extends MetadataLoader {
+
+ private val fetch = Cache.fetch()
+
+ override def getVersions(module: coursier.Module): List[Version] = {
+ // TODO fallback to 'versionsFromListing' if 'versions' doesn't work? (needs to be made public in coursier first)
+ val allVersions = mavenRepo.versions(module, fetch).run.unsafePerformSync
+ allVersions
+ .map(_.available.map(Version(_)))
+ .getOrElse(List.empty)
+ }
+}
diff --git a/scalalib/src/mill/scalalib/dependency/metadata/MetadataLoader.scala b/scalalib/src/mill/scalalib/dependency/metadata/MetadataLoader.scala
new file mode 100644
index 00000000..20271f0e
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/metadata/MetadataLoader.scala
@@ -0,0 +1,7 @@
+package mill.scalalib.dependency.metadata
+
+import mill.scalalib.dependency.versions.Version
+
+private[dependency] trait MetadataLoader {
+ def getVersions(module: coursier.Module): Seq[Version]
+}
diff --git a/scalalib/src/mill/scalalib/dependency/metadata/MetadataLoaderFactory.scala b/scalalib/src/mill/scalalib/dependency/metadata/MetadataLoaderFactory.scala
new file mode 100644
index 00000000..4495d6b0
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/metadata/MetadataLoaderFactory.scala
@@ -0,0 +1,11 @@
+package mill.scalalib.dependency.metadata
+
+import coursier.Repository
+import coursier.maven.MavenRepository
+
+private[dependency] object MetadataLoaderFactory {
+ def apply(repo: Repository): Option[MetadataLoader] = repo match {
+ case mavenRepo: MavenRepository => Some(MavenMetadataLoader(mavenRepo))
+ case _ => None
+ }
+}
diff --git a/scalalib/src/mill/scalalib/dependency/updates/ModuleDependenciesUpdates.scala b/scalalib/src/mill/scalalib/dependency/updates/ModuleDependenciesUpdates.scala
new file mode 100644
index 00000000..a989cd31
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/updates/ModuleDependenciesUpdates.scala
@@ -0,0 +1,15 @@
+package mill.scalalib.dependency.updates
+
+import mill.scalalib.JavaModule
+import mill.scalalib.dependency.versions.Version
+
+import scala.collection.SortedSet
+
+private[dependency] final case class ModuleDependenciesUpdates(
+ module: JavaModule,
+ dependencies: Seq[DependencyUpdates])
+
+private[dependency] final case class DependencyUpdates(
+ dependency: coursier.Dependency,
+ currentVersion: Version,
+ updates: SortedSet[Version])
diff --git a/scalalib/src/mill/scalalib/dependency/updates/UpdatesFinder.scala b/scalalib/src/mill/scalalib/dependency/updates/UpdatesFinder.scala
new file mode 100644
index 00000000..3430592f
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/updates/UpdatesFinder.scala
@@ -0,0 +1,75 @@
+/*
+ * This file contains code originally published under the following license:
+ *
+ * Copyright (c) 2012, Roman Timushev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package mill.scalalib.dependency.updates
+
+import mill.scalalib.dependency.versions._
+
+import scala.collection.SortedSet
+
+private[dependency] object UpdatesFinder {
+
+ import scala.Ordered._
+
+ def findUpdates(dependencyVersions: ModuleDependenciesVersions,
+ allowPreRelease: Boolean): ModuleDependenciesUpdates = {
+ val dependencies =
+ dependencyVersions.dependencies.map { dependencyVersion =>
+ findUpdates(dependencyVersion, allowPreRelease)
+ }
+ ModuleDependenciesUpdates(dependencyVersions.module, dependencies)
+ }
+
+ def findUpdates(dependencyVersion: DependencyVersions,
+ allowPreRelease: Boolean): DependencyUpdates = {
+ val current = dependencyVersion.currentVersion
+ val versions = dependencyVersion.allversions.to[SortedSet]
+
+ val updates = versions
+ .filter(isUpdate(current))
+ .filterNot(lessStable(current, allowPreRelease))
+
+ DependencyUpdates(dependencyVersion.dependency,
+ dependencyVersion.currentVersion,
+ updates)
+ }
+
+ private def lessStable(current: Version, allowPreRelease: Boolean)(
+ another: Version): Boolean = (current, another) match {
+ case (ReleaseVersion(_), ReleaseVersion(_)) => false
+ case (SnapshotVersion(_, _, _), _) => false
+ case (_, SnapshotVersion(_, _, _)) => true
+ case (ReleaseVersion(_), PreReleaseVersion(_, _)) => !allowPreRelease
+ case (ReleaseVersion(_), PreReleaseBuildVersion(_, _, _)) =>
+ !allowPreRelease
+ case (ReleaseVersion(_), _) => true
+ case (_, _) => false
+ }
+
+ private def isUpdate(current: Version) = current < _
+}
diff --git a/scalalib/src/mill/scalalib/dependency/versions/ModuleDependenciesVersions.scala b/scalalib/src/mill/scalalib/dependency/versions/ModuleDependenciesVersions.scala
new file mode 100644
index 00000000..12d57059
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/versions/ModuleDependenciesVersions.scala
@@ -0,0 +1,12 @@
+package mill.scalalib.dependency.versions
+
+import mill.scalalib.JavaModule
+
+private[dependency] final case class ModuleDependenciesVersions(
+ module: JavaModule,
+ dependencies: Seq[DependencyVersions])
+
+private[dependency] final case class DependencyVersions(
+ dependency: coursier.Dependency,
+ currentVersion: Version,
+ allversions: Set[Version])
diff --git a/scalalib/src/mill/scalalib/dependency/versions/Version.scala b/scalalib/src/mill/scalalib/dependency/versions/Version.scala
new file mode 100644
index 00000000..a2719023
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/versions/Version.scala
@@ -0,0 +1,227 @@
+/*
+ * This file contains code originally published under the following license:
+ *
+ * Copyright (c) 2012, Roman Timushev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package mill.scalalib.dependency.versions
+
+import scala.util.matching.Regex
+import scala.util.matching.Regex.Groups
+
+private[dependency] sealed trait Version {
+ def major: Long
+
+ def minor: Long
+
+ def patch: Long
+}
+
+private[dependency] case class ValidVersion(text: String,
+ releasePart: List[Long],
+ preReleasePart: List[String],
+ buildPart: List[String])
+ extends Version {
+ def major: Long = releasePart.headOption getOrElse 0
+
+ def minor: Long = releasePart.drop(1).headOption getOrElse 1
+
+ def patch: Long = releasePart.drop(2).headOption getOrElse 1
+
+ override def toString: String = text
+}
+
+private[dependency] case class InvalidVersion(text: String) extends Version {
+ def major: Long = -1
+
+ def minor: Long = -1
+
+ def patch: Long = -1
+}
+
+private[dependency] object ReleaseVersion {
+ private val releaseKeyword: Regex = "(?i)final|release".r
+
+ def unapply(v: Version): Option[List[Long]] = v match {
+ case ValidVersion(_, releasePart, Nil, Nil) => Some(releasePart)
+ case ValidVersion(_, releasePart, releaseKeyword() :: Nil, Nil) =>
+ Some(releasePart)
+ case _ => None
+ }
+}
+
+private[dependency] object PreReleaseVersion {
+ def unapply(v: Version): Option[(List[Long], List[String])] = v match {
+ case ValidVersion(_, releasePart, preReleasePart, Nil)
+ if preReleasePart.nonEmpty =>
+ Some(releasePart, preReleasePart)
+ case _ => None
+ }
+}
+
+private[dependency] object PreReleaseBuildVersion {
+ def unapply(v: Version): Option[(List[Long], List[String], List[String])] =
+ v match {
+ case ValidVersion(_, releasePart, preReleasePart, buildPart)
+ if preReleasePart.nonEmpty && buildPart.nonEmpty =>
+ Some(releasePart, preReleasePart, buildPart)
+ case _ => None
+ }
+}
+
+private[dependency] object SnapshotVersion {
+ def unapply(v: Version): Option[(List[Long], List[String], List[String])] =
+ v match {
+ case ValidVersion(_, releasePart, preReleasePart, buildPart)
+ if preReleasePart.lastOption.contains("SNAPSHOT") =>
+ Some(releasePart, preReleasePart, buildPart)
+ case _ => None
+ }
+}
+
+private[dependency] object BuildVersion {
+ def unapply(v: Version): Option[(List[Long], List[String])] = v match {
+ case ValidVersion(_, releasePart, Nil, buildPart) if buildPart.nonEmpty =>
+ Some(releasePart, buildPart)
+ case _ => None
+ }
+}
+
+private[dependency] object Version {
+ def apply(text: String): Version = synchronized {
+ VersionParser
+ .parse(text)
+ .fold(
+ (_, _, _) => InvalidVersion(text),
+ { case ((a, b, c), _) => ValidVersion(text, a.toList, b.toList, c.toList)}
+ )
+ }
+
+ implicit def versionOrdering: Ordering[Version] = VersionOrdering
+}
+
+private[dependency] object VersionOrdering extends Ordering[Version] {
+
+ private val subParts = "(\\d+)?(\\D+)?".r
+
+ private def parsePart(s: String): Seq[Either[Int, String]] =
+ try {
+ subParts
+ .findAllIn(s)
+ .matchData
+ .flatMap {
+ case Groups(num, str) =>
+ Seq(Option(num).map(_.toInt).map(Left.apply),
+ Option(str).map(Right.apply))
+ }
+ .flatten
+ .toList
+ } catch {
+ case _: NumberFormatException => List(Right(s))
+ }
+
+ private def toOpt(x: Int): Option[Int] = if (x == 0) None else Some(x)
+
+ private def comparePart(a: String, b: String) = {
+ if (a == b) None
+ else
+ (parsePart(a) zip parsePart(b)) map {
+ case (Left(x), Left(y)) => x compareTo y
+ case (Left(_), Right(_)) => -1
+ case (Right(_), Left(_)) => 1
+ case (Right(x), Right(y)) => x compareTo y
+ } find (0 != _) orElse Some(a compareTo b)
+ }
+
+ private def compareNumericParts(a: List[Long], b: List[Long]): Option[Int] =
+ (a, b) match {
+ case (ah :: at, bh :: bt) =>
+ toOpt(ah compareTo bh) orElse compareNumericParts(at, bt)
+ case (ah :: at, Nil) =>
+ toOpt(ah compareTo 0L) orElse compareNumericParts(at, Nil)
+ case (Nil, bh :: bt) =>
+ toOpt(0L compareTo bh) orElse compareNumericParts(Nil, bt)
+ case (Nil, Nil) =>
+ None
+ }
+
+ private def compareParts(a: List[String], b: List[String]): Option[Int] =
+ (a, b) match {
+ case (ah :: at, bh :: bt) =>
+ comparePart(ah, bh) orElse compareParts(at, bt)
+ case (_ :: _, Nil) =>
+ Some(1)
+ case (Nil, _ :: _) =>
+ Some(-1)
+ case (Nil, Nil) =>
+ None
+ }
+
+ def compare(x: Version, y: Version): Int = (x, y) match {
+ case (InvalidVersion(a), InvalidVersion(b)) =>
+ a compareTo b
+ case (InvalidVersion(_), _) =>
+ -1
+ case (_, InvalidVersion(_)) =>
+ 1
+ case (ReleaseVersion(r1), ReleaseVersion(r2)) =>
+ compareNumericParts(r1, r2) getOrElse 0
+ case (ReleaseVersion(r1), PreReleaseVersion(r2, p2)) =>
+ compareNumericParts(r1, r2) getOrElse 1
+ case (ReleaseVersion(r1), PreReleaseBuildVersion(r2, p2, b2)) =>
+ compareNumericParts(r1, r2) getOrElse 1
+ case (ReleaseVersion(r1), BuildVersion(r2, b2)) =>
+ compareNumericParts(r1, r2) getOrElse -1
+ case (PreReleaseVersion(r1, p1), ReleaseVersion(r2)) =>
+ compareNumericParts(r1, r2) getOrElse -1
+ case (PreReleaseVersion(r1, p1), PreReleaseVersion(r2, p2)) =>
+ compareNumericParts(r1, r2) orElse compareParts(p1, p2) getOrElse 0
+ case (PreReleaseVersion(r1, p1), PreReleaseBuildVersion(r2, p2, b2)) =>
+ compareNumericParts(r1, r2) orElse compareParts(p1, p2) getOrElse -1
+ case (PreReleaseVersion(r1, p1), BuildVersion(r2, b2)) =>
+ compareNumericParts(r1, r2) getOrElse -1
+ case (PreReleaseBuildVersion(r1, p1, b1), ReleaseVersion(r2)) =>
+ compareNumericParts(r1, r2) getOrElse -1
+ case (PreReleaseBuildVersion(r1, p1, b1), PreReleaseVersion(r2, p2)) =>
+ compareNumericParts(r1, r2) orElse compareParts(p1, p2) getOrElse 1
+ case (PreReleaseBuildVersion(r1, p1, b1),
+ PreReleaseBuildVersion(r2, p2, b2)) =>
+ compareNumericParts(r1, r2) orElse
+ compareParts(p1, p2) orElse
+ compareParts(b1, b2) getOrElse
+ 0
+ case (PreReleaseBuildVersion(r1, p1, b1), BuildVersion(r2, b2)) =>
+ compareNumericParts(r1, r2) getOrElse -1
+ case (BuildVersion(r1, b1), ReleaseVersion(r2)) =>
+ compareNumericParts(r1, r2) getOrElse 1
+ case (BuildVersion(r1, b1), PreReleaseVersion(r2, p2)) =>
+ compareNumericParts(r1, r2) getOrElse 1
+ case (BuildVersion(r1, b1), PreReleaseBuildVersion(r2, p2, b2)) =>
+ compareNumericParts(r1, r2) getOrElse 1
+ case (BuildVersion(r1, b1), BuildVersion(r2, b2)) =>
+ compareNumericParts(r1, r2) orElse compareParts(b1, b2) getOrElse 0
+ }
+
+}
diff --git a/scalalib/src/mill/scalalib/dependency/versions/VersionParser.scala b/scalalib/src/mill/scalalib/dependency/versions/VersionParser.scala
new file mode 100644
index 00000000..d85c4276
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/versions/VersionParser.scala
@@ -0,0 +1,30 @@
+package mill.scalalib.dependency.versions
+
+import fastparse.all._
+
+private[dependency] object VersionParser {
+
+ private val numberParser =
+ P(CharIn('0' to '9').rep(1).!.map(_.toLong))
+ private val numericPartParser =
+ P(numberParser ~ &(CharIn(".", "-", "+") | End)).rep(min = 1, sep = ".")
+
+ private val tokenParser =
+ CharPred(c => c != '.' && c != '-' && c != '+').rep(1).!
+ private val tokenPartParser =
+ tokenParser.rep(sep = CharIn(".", "-"))
+
+ private val firstPartParser =
+ P(CharIn(".", "-") ~ tokenPartParser).?
+
+ private val secondPartParser =
+ P("+" ~ tokenPartParser).?
+
+ private val versionParser =
+ P(numericPartParser ~ firstPartParser ~ secondPartParser).map {
+ case (a, b, c) => (a, b.getOrElse(Seq.empty), c.getOrElse(Seq.empty))
+ }
+
+ def parse(text: String): Parsed[(Seq[Long], Seq[String], Seq[String])] =
+ versionParser.parse(text)
+}
diff --git a/scalalib/src/mill/scalalib/dependency/versions/VersionsFinder.scala b/scalalib/src/mill/scalalib/dependency/versions/VersionsFinder.scala
new file mode 100644
index 00000000..efb8cb64
--- /dev/null
+++ b/scalalib/src/mill/scalalib/dependency/versions/VersionsFinder.scala
@@ -0,0 +1,74 @@
+package mill.scalalib.dependency.versions
+
+import ammonite.ops.pwd
+import mill.define.{BaseModule, Task}
+import mill.eval.Evaluator
+import mill.scalalib.dependency.metadata.MetadataLoaderFactory
+import mill.scalalib.{Dep, JavaModule, Lib}
+import mill.util.Ctx.{Home, Log}
+import mill.util.{Loose, Strict}
+
+private[dependency] object VersionsFinder {
+
+ def findVersions(ctx: Log with Home,
+ rootModule: BaseModule): Seq[ModuleDependenciesVersions] = {
+ val evaluator =
+ new Evaluator(ctx.home, pwd / 'out, pwd / 'out, rootModule, ctx.log)
+
+ val javaModules = rootModule.millInternal.modules.collect {
+ case javaModule: JavaModule => javaModule
+ }
+
+ val resolvedDependencies = resolveDependencies(evaluator, javaModules)
+ resolveVersions(resolvedDependencies)
+ }
+
+ private def resolveDependencies(evaluator: Evaluator[_],
+ javaModules: Seq[JavaModule]) =
+ javaModules.map { javaModule =>
+ val depToDependency =
+ eval(evaluator, javaModule.resolveCoursierDependency)
+ val deps = evalOrElse(evaluator, javaModule.ivyDeps, Loose.Agg.empty[Dep])
+
+ val (dependencies, _) =
+ Lib.resolveDependenciesMetadata(javaModule.repositories,
+ depToDependency,
+ deps)
+
+ (javaModule, dependencies)
+ }
+
+ private def resolveVersions(resolvedDependencies: Seq[ResolvedDependencies]) =
+ resolvedDependencies.map {
+ case (javaModule, dependencies) =>
+ val metadataLoaders =
+ javaModule.repositories.flatMap(MetadataLoaderFactory(_))
+
+ val versions = dependencies.map { dependency =>
+ val currentVersion = Version(dependency.version)
+ val allVersions =
+ metadataLoaders
+ .flatMap(_.getVersions(dependency.module))
+ .toSet
+ DependencyVersions(dependency, currentVersion, allVersions)
+ }
+
+ ModuleDependenciesVersions(javaModule, versions)
+ }
+
+ private def eval[T](evaluator: Evaluator[_], e: Task[T]): T =
+ evaluator.evaluate(Strict.Agg(e)).values match {
+ case Seq() => throw new NoSuchElementException
+ case Seq(e: T) => e
+ }
+
+ private def evalOrElse[T](evaluator: Evaluator[_],
+ e: Task[T],
+ default: => T): T =
+ evaluator.evaluate(Strict.Agg(e)).values match {
+ case Seq() => default
+ case Seq(e: T) => e
+ }
+
+ private type ResolvedDependencies = (JavaModule, Seq[coursier.Dependency])
+}
diff --git a/scalalib/test/src/mill/scalalib/dependency/metadata/MetadataLoaderFactoryTests.scala b/scalalib/test/src/mill/scalalib/dependency/metadata/MetadataLoaderFactoryTests.scala
new file mode 100644
index 00000000..8187976c
--- /dev/null
+++ b/scalalib/test/src/mill/scalalib/dependency/metadata/MetadataLoaderFactoryTests.scala
@@ -0,0 +1,64 @@
+/*
+ * This file contains code originally published under the following license:
+ *
+ * Copyright (c) 2012, Roman Timushev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package mill.scalalib.dependency.metadata
+
+import coursier.Fetch.Content
+import coursier.core.{Artifact, Module, Project, Repository}
+import coursier.ivy.IvyRepository
+import coursier.maven.MavenRepository
+import scalaz.{EitherT, Monad}
+import utest._
+
+object MetadataLoaderFactoryTests extends TestSuite {
+
+ val tests = Tests {
+ 'mavenRepository - {
+ val mavenRepo = MavenRepository("https://repo1.maven.org/maven2")
+ assertMatch(MetadataLoaderFactory(mavenRepo)) {
+ case Some(MavenMetadataLoader(`mavenRepo`)) =>
+ }
+ }
+ 'ivyRepository - {
+ val ivyRepo = IvyRepository(
+ "https://dl.bintray.com/sbt/sbt-plugin-releases/" + coursier.ivy.Pattern.default.string,
+ dropInfoAttributes = true)
+ assertMatch(MetadataLoaderFactory(ivyRepo)) { case None => }
+ }
+ 'otherRepository - {
+ val otherRepo = new CustomRepository
+ assertMatch(MetadataLoaderFactory(otherRepo)) { case None => }
+ }
+ }
+
+ case class CustomRepository() extends Repository {
+ override def find[F[_]](module: Module, version: String, fetch: Content[F])(
+ implicit F: Monad[F]): EitherT[F, String, (Artifact.Source, Project)] =
+ ???
+ }
+}
diff --git a/scalalib/test/src/mill/scalalib/dependency/updates/UpdatesFinderTests.scala b/scalalib/test/src/mill/scalalib/dependency/updates/UpdatesFinderTests.scala
new file mode 100644
index 00000000..7b6e6e36
--- /dev/null
+++ b/scalalib/test/src/mill/scalalib/dependency/updates/UpdatesFinderTests.scala
@@ -0,0 +1,173 @@
+/*
+ * This file contains code originally published under the following license:
+ *
+ * Copyright (c) 2012, Roman Timushev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package mill.scalalib.dependency.updates
+
+import mill.scalalib.dependency.versions.{DependencyVersions, Version}
+import utest._
+
+object UpdatesFinderTests extends TestSuite {
+
+ private def updates(current: String,
+ available: Seq[String],
+ allowPreRelease: Boolean) = {
+ val dependency = coursier.Dependency(
+ coursier.Module("com.example.organization", "example-artifact"),
+ current)
+ val currentVersion = Version(current)
+ val allVersions = available.map(Version(_)).toSet
+
+ UpdatesFinder
+ .findUpdates(DependencyVersions(dependency, currentVersion, allVersions),
+ allowPreRelease)
+ .updates
+ .map(_.toString)
+ }
+
+ val available = Seq(
+ "0.9.9-SNAPSHOT",
+ "0.9.9-M3",
+ "0.9.9",
+ "1.0.0-SNAPSHOT",
+ "1.0.0-M2",
+ "1.0.0-M3",
+ "1.0.0",
+ "1.0.1-SNAPSHOT",
+ "1.0.1-M3",
+ "1.0.1"
+ )
+
+ val tests = Tests {
+
+ 'snapshotArtifacts - {
+ val u = updates("1.0.0-SNAPSHOT", available, allowPreRelease = false)
+ val pu = updates("1.0.0-SNAPSHOT", available, allowPreRelease = true)
+
+ 'noOldStableVersions - {
+ assert(!u.contains("0.9.9"))
+ }
+ 'noOldMilestones - {
+ assert(!u.contains("0.9.9-M3"))
+ }
+ 'noOldSnapshots - {
+ assert(!u.contains("0.9.9-SNAPSHOT"))
+ }
+ 'noCurrentMilestones - {
+ assert(!u.contains("1.0.0-M3"))
+ }
+ 'noCurrentSnapshot - {
+ assert(!u.contains("1.0.0-SNAPSHOT"))
+ }
+ 'stableUpdates - {
+ assert(u.contains("1.0.0") && u.contains("1.0.1"))
+ }
+ 'milestoneUpdates - {
+ assert(u.contains("1.0.1-M3"))
+ }
+ 'snapshotUpdates - {
+ assert(u.contains("1.0.1-SNAPSHOT"))
+ }
+ 'noDifferencesRegardingOptionalPreReleases - {
+ assert(u == pu)
+ }
+ }
+
+ 'milestoneArtifacts - {
+ val u = updates("1.0.0-M2", available, allowPreRelease = false)
+ val pu = updates("1.0.0-M2", available, allowPreRelease = true)
+
+ 'noOldStableVersions - {
+ assert(!u.contains("0.9.9"))
+ }
+ 'noOldSnapshots - {
+ assert(!u.contains("0.9.9-SNAPSHOT"))
+ }
+ 'noOldMilestones - {
+ assert(!u.contains("0.9.9-M3"))
+ }
+ 'noCurrentSnapshot - {
+ assert(!u.contains("1.0.0-SNAPSHOT"))
+ }
+ 'currentMilestones - {
+ assert(u.contains("1.0.0-M3"))
+ }
+ 'stableUpdates - {
+ assert(u.contains("1.0.1"))
+ }
+ 'noSnapshotUpdates - {
+ assert(!u.contains("1.0.1-SNAPSHOT"))
+ }
+ 'milestoneUpdates - {
+ assert(u.contains("1.0.1-M3"))
+ }
+ 'noDifferencesRegardingOptionalPreReleases - {
+ assert(u == pu)
+ }
+ }
+
+ 'stableArtifacts - {
+ val u = updates("1.0.0", available, allowPreRelease = false)
+ val pu = updates("1.0.0", available, allowPreRelease = true)
+
+ 'noOldStableVersions - {
+ assert(!u.contains("0.9.9"))
+ assert(!pu.contains("0.9.9"))
+ }
+ 'noOldSnapshots - {
+ assert(!u.contains("0.9.9-SNAPSHOT"))
+ assert(!pu.contains("0.9.9-SNAPSHOT"))
+ }
+ 'noOldMilestones - {
+ assert(!u.contains("0.9.9-M3"))
+ assert(!pu.contains("0.9.9-M3"))
+ }
+ 'noCurrentSnapshot - {
+ assert(!u.contains("1.0.0-SNAPSHOT"))
+ assert(!pu.contains("1.0.0-SNAPSHOT"))
+ }
+ 'noCurrentMilestones - {
+ assert(!u.contains("1.0.0-M3"))
+ assert(!pu.contains("1.0.0-M3"))
+ }
+ 'stableUpdates - {
+ assert(u.contains("1.0.1"))
+ assert(pu.contains("1.0.1"))
+ }
+ 'noSnapshotUpdates - {
+ assert(!u.contains("1.0.1-SNAPSHOT"))
+ assert(!pu.contains("1.0.1-SNAPSHOT"))
+ }
+ 'noMilestoneUpdates - {
+ assert(!u.contains("1.0.1-M3"))
+ }
+ 'milestoneUpdatesWhenAllowingPreReleases - {
+ assert(pu.contains("1.0.1-M3"))
+ }
+ }
+ }
+}
diff --git a/scalalib/test/src/mill/scalalib/dependency/versions/VersionTests.scala b/scalalib/test/src/mill/scalalib/dependency/versions/VersionTests.scala
new file mode 100644
index 00000000..8bc1a992
--- /dev/null
+++ b/scalalib/test/src/mill/scalalib/dependency/versions/VersionTests.scala
@@ -0,0 +1,139 @@
+/*
+ * This file contains code originally published under the following license:
+ *
+ * Copyright (c) 2012, Roman Timushev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package mill.scalalib.dependency.versions
+
+import utest._
+import fastparse.core.Parsed
+
+object VersionTests extends TestSuite {
+
+ val tests = Tests {
+ 'versionsClassification - {
+ 'ReleaseVersion - {
+ List("1.0.0", "1.0.0.Final", "1.0.0-FINAL", "1.0.0.RELEASE") foreach {
+ rel =>
+ assertMatch(Version(rel)) {
+ case ReleaseVersion(List(1, 0, 0)) =>
+ }
+ }
+ }
+ 'PreReleaseVersion - {
+ assertMatch(Version("1.0.0-alpha.1")) {
+ case PreReleaseVersion(List(1, 0, 0), List("alpha", "1")) =>
+ }
+ }
+ 'PreReleaseBuildVersion - {
+ assertMatch(Version("1.0.0-alpha.1+build.10")) {
+ case PreReleaseBuildVersion(List(1, 0, 0),
+ List("alpha", "1"),
+ List("build", "10")) =>
+ }
+ }
+ 'BuildVersion - {
+ assertMatch(Version("1.0.0+build.10")) {
+ case BuildVersion(List(1, 0, 0), List("build", "10")) =>
+ }
+ }
+ }
+
+ 'semverVersionsOrdering - {
+ import scala.Ordered._
+
+ val v = List(
+ "invalid",
+ "1.0.0-20131213005945",
+ "1.0.0-alpha",
+ "1.0.0-alpha.1",
+ "1.0.0-beta.2",
+ "1.0.0-beta.11",
+ "1.0.0-rc.1",
+ "1.0.0-rc.1+build.1",
+ "1.0.0",
+ "1.0.0+0.3.7",
+ "1.33.7+build",
+ "1.33.7+build.2.b8f12d7",
+ "1.33.7+build.11.e0f985a",
+ "2.0.M5b",
+ "2.0.M6-SNAP9",
+ "2.0.M6-SNAP23",
+ "2.0.M6-SNAP23a"
+ ).map(Version.apply)
+ val pairs = v.tails.flatMap {
+ case h :: t => t.map((h, _))
+ case Nil => List.empty
+ }
+ pairs.foreach {
+ case (a, b) =>
+ assert(a < b)
+ assert(b > a)
+ }
+ }
+
+ 'parser - {
+
+ Symbol("parse 1.0.5") - {
+ assertMatch(VersionParser.parse("1.0.5")) {
+ case Parsed.Success((Seq(1, 0, 5), Seq(), Seq()), _) =>
+ }
+ }
+
+ Symbol("parse 1.0.M3") - {
+ assertMatch(VersionParser.parse("1.0.M3")) {
+ case Parsed.Success((Seq(1, 0), Seq("M3"), Seq()), _) =>
+ }
+ }
+ Symbol("parse 1.0.3m") - {
+ assertMatch(VersionParser.parse("1.0.3m")) {
+ case Parsed.Success((Seq(1, 0), Seq("3m"), Seq()), _) =>
+ }
+ }
+ Symbol("parse 1.0.3m.4") - {
+ assertMatch(VersionParser.parse("1.0.3m.4")) {
+ case Parsed.Success((Seq(1, 0), Seq("3m", "4"), Seq()), _) =>
+ }
+ }
+ Symbol("parse 9.1-901-1.jdbc4") - {
+ assertMatch(VersionParser.parse("9.1-901-1.jdbc4")) {
+ case Parsed
+ .Success((Seq(9, 1), Seq("901", "1", "jdbc4"), Seq()), _) =>
+ }
+ }
+ Symbol("parse 1.33.7+build/11.e0f985a") - {
+ assertMatch(VersionParser.parse("1.33.7+build/11.e0f985a")) {
+ case Parsed.Success((Seq(1, 33, 7), Seq(), Seq("build/11", "e0f985a")), _) =>
+ }
+ }
+ Symbol("parse 9.1-901-1.jdbc4+build/11.e0f985a") - {
+ assertMatch(VersionParser.parse("9.1-901-1.jdbc4+build/11.e0f985a")) {
+ case Parsed.Success((Seq(9, 1), Seq("901", "1", "jdbc4"), Seq("build/11", "e0f985a")), _) =>
+ }
+ }
+ }
+ }
+}