From 5616dc9b46e033b39d0df12e42173ee1c875cee4 Mon Sep 17 00:00:00 2001 From: Guillaume Galy Date: Sat, 14 Jul 2018 03:20:19 +0200 Subject: 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 --- docs/pages/1 - Intro to Mill.md | 23 +++ scalalib/src/mill/scalalib/Dependency.scala | 22 ++ .../dependency/DependencyUpdatesImpl.scala | 52 +++++ .../dependency/metadata/MavenMetadataLoader.scala | 19 ++ .../dependency/metadata/MetadataLoader.scala | 7 + .../metadata/MetadataLoaderFactory.scala | 11 + .../updates/ModuleDependenciesUpdates.scala | 15 ++ .../dependency/updates/UpdatesFinder.scala | 75 +++++++ .../versions/ModuleDependenciesVersions.scala | 12 ++ .../scalalib/dependency/versions/Version.scala | 227 +++++++++++++++++++++ .../dependency/versions/VersionParser.scala | 30 +++ .../dependency/versions/VersionsFinder.scala | 74 +++++++ .../metadata/MetadataLoaderFactoryTests.scala | 64 ++++++ .../dependency/updates/UpdatesFinderTests.scala | 173 ++++++++++++++++ .../dependency/versions/VersionTests.scala | 139 +++++++++++++ 15 files changed, 943 insertions(+) create mode 100644 scalalib/src/mill/scalalib/Dependency.scala create mode 100644 scalalib/src/mill/scalalib/dependency/DependencyUpdatesImpl.scala create mode 100644 scalalib/src/mill/scalalib/dependency/metadata/MavenMetadataLoader.scala create mode 100644 scalalib/src/mill/scalalib/dependency/metadata/MetadataLoader.scala create mode 100644 scalalib/src/mill/scalalib/dependency/metadata/MetadataLoaderFactory.scala create mode 100644 scalalib/src/mill/scalalib/dependency/updates/ModuleDependenciesUpdates.scala create mode 100644 scalalib/src/mill/scalalib/dependency/updates/UpdatesFinder.scala create mode 100644 scalalib/src/mill/scalalib/dependency/versions/ModuleDependenciesVersions.scala create mode 100644 scalalib/src/mill/scalalib/dependency/versions/Version.scala create mode 100644 scalalib/src/mill/scalalib/dependency/versions/VersionParser.scala create mode 100644 scalalib/src/mill/scalalib/dependency/versions/VersionsFinder.scala create mode 100644 scalalib/test/src/mill/scalalib/dependency/metadata/MetadataLoaderFactoryTests.scala create mode 100644 scalalib/test/src/mill/scalalib/dependency/updates/UpdatesFinderTests.scala create mode 100644 scalalib/test/src/mill/scalalib/dependency/versions/VersionTests.scala 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")), _) => + } + } + } + } +} -- cgit v1.2.3