From e20b0287fda51c0c887ca803a9f564e3ffb21635 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Mon, 3 Sep 2018 12:42:13 +0200 Subject: [PATCH] Utility to check that same version of all modules is used * Stolen from Cinnamon * Can be used from outside of Akka, e.g. Akka HTTP or Lagom --- .../akka/util/ManifestInfoVersionSpec.scala | 38 ++++ .../main/scala/akka/actor/ActorSystem.scala | 28 +++ .../main/scala/akka/util/ManifestInfo.scala | 187 ++++++++++++++++++ build.sbt | 1 + 4 files changed, 254 insertions(+) create mode 100644 akka-actor-tests/src/test/scala/akka/util/ManifestInfoVersionSpec.scala create mode 100644 akka-actor/src/main/scala/akka/util/ManifestInfo.scala diff --git a/akka-actor-tests/src/test/scala/akka/util/ManifestInfoVersionSpec.scala b/akka-actor-tests/src/test/scala/akka/util/ManifestInfoVersionSpec.scala new file mode 100644 index 0000000000..b7a7be8873 --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/util/ManifestInfoVersionSpec.scala @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2015–2018 Lightbend Inc. + */ + +package akka.util + +import org.scalatest.Matchers +import org.scalatest.WordSpec +import akka.util.ManifestInfo.Version + +class ManifestInfoVersionSpec extends WordSpec with Matchers { + + "Version" should { + + "compare full version" in { + new Version("1.2.3") should ===(new Version("1.2.3")) + new Version("1.2.3") should !==(new Version("1.2.4")) + new Version("1.2.4") should be > new Version("1.2.3") + new Version("3.2.1") should be > new Version("1.2.3") + new Version("3.2.1") should be < new Version("3.3.1") + } + + "compare partial version" in { + new Version("1.2") should ===(new Version("1.2")) + new Version("1.2") should !==(new Version("1.3")) + new Version("1.2.1") should be > new Version("1.2") + new Version("2.4") should be > new Version("2.3") + new Version("3.2") should be < new Version("3.2.7") + } + + "compare extra" in { + new Version("1.2.3-foo") should ===(new Version("1.2.3-foo")) + new Version("1.2.3-foo") should !==(new Version("1.2.3-bar")) + new Version("1.2-foo") should be > new Version("1.2.3") + new Version("1.2.3-foo") should be > new Version("1.2.3-bar") + } + } +} diff --git a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala index 24fb366e9e..e5196aef70 100644 --- a/akka-actor/src/main/scala/akka/actor/ActorSystem.scala +++ b/akka-actor/src/main/scala/akka/actor/ActorSystem.scala @@ -822,6 +822,33 @@ private[akka] class ActorSystemImpl( def /(actorName: String): ActorPath = guardian.path / actorName def /(path: Iterable[String]): ActorPath = guardian.path / path + // Used for ManifestInfo.checkSameVersion + private def allModules: List[String] = List( + "akka-actor", + "akka-actor-testkit-typed", + "akka-actor-typed", + "akka-agent", + "akka-camel", + "akka-cluster", + "akka-cluster-metrics", + "akka-cluster-sharding", + "akka-cluster-sharding-typed", + "akka-cluster-tools", + "akka-cluster-typed", + "akka-distributed-data", + "akka-multi-node-testkit", + "akka-osgi", + "akka-persistence", + "akka-persistence-query", + "akka-persistence-shared", + "akka-persistence-typed", + "akka-protobuf", + "akka-remote", + "akka-slf4j", + "akka-stream", + "akka-stream-testkit", + "akka-stream-typed") + @volatile private var _initialized = false /** * Asserts that the ActorSystem has been fully initialized. Can be used to guard code blocks that might accidentally @@ -844,6 +871,7 @@ private[akka] class ActorSystemImpl( if (settings.LogDeadLetters > 0) logDeadLetterListener = Some(systemActorOf(Props[DeadLetterListener], "deadLetterListener")) eventStream.startUnsubscriber() + ManifestInfo(this).checkSameVersion("Akka", allModules, logWarning = true) loadExtensions() if (LogConfigOnStart) logConfiguration() this diff --git a/akka-actor/src/main/scala/akka/util/ManifestInfo.scala b/akka-actor/src/main/scala/akka/util/ManifestInfo.scala new file mode 100644 index 0000000000..f541217e31 --- /dev/null +++ b/akka-actor/src/main/scala/akka/util/ManifestInfo.scala @@ -0,0 +1,187 @@ +/** + * Copyright (C) 2015–2018 Lightbend Inc. + */ + +package akka.util + +import scala.collection.immutable +import java.io.IOException +import java.util.Arrays +import java.util.jar.Attributes +import java.util.jar.Manifest + +import akka.actor.ActorSystem +import akka.actor.ExtendedActorSystem +import akka.actor.Extension +import akka.actor.ExtensionId +import akka.actor.ExtensionIdProvider +import akka.annotation.InternalApi +import akka.event.Logging + +/** + * Akka extension that extracts [[ManifestInfo.Version]] information from META-INF/MANIFEST.MF in jar files + * on the classpath of the `ClassLoader` of the `ActorSystem`. + */ +object ManifestInfo extends ExtensionId[ManifestInfo] with ExtensionIdProvider { + private val ImplTitle = "Implementation-Title" + private val ImplVersion = "Implementation-Version" + private val ImplVendor = "Implementation-Vendor-Id" + + private val BundleName = "Bundle-Name" + private val BundleVersion = "Bundle-Version" + private val BundleVendor = "Bundle-Vendor" + + private val knownVendors = Set( + "com.typesafe.akka", + "com.lightbend.akka", + "Lightbend Inc.", + "Lightbend", + "com.lightbend.lagom", + "com.typesafe.play" + ) + + override def get(system: ActorSystem): ManifestInfo = super.get(system) + + override def lookup(): ManifestInfo.type = ManifestInfo + + override def createExtension(system: ExtendedActorSystem): ManifestInfo = new ManifestInfo(system) + + /** + * Comparable version information + */ + final class Version(val version: String) extends Comparable[Version] { + private val (numbers: Array[Int], rest: String) = { + val numbers = new Array[Int](3) + val segments: Array[String] = version.split("[.-]") + var segmentPos = 0 + var numbersPos = 0 + while (numbersPos < 3) { + if (segmentPos < segments.length) try { + numbers(numbersPos) = segments(segmentPos).toInt + segmentPos += 1 + } catch { + case e: NumberFormatException ⇒ + // This means that we have a trailing part on the version string and + // less than 3 numbers, so we assume that this is a "newer" version + numbers(numbersPos) = Integer.MAX_VALUE + } + numbersPos += 1 + } + + val rest: String = + if (segmentPos >= segments.length) "" + else String.join("-", Arrays.asList(Arrays.copyOfRange(segments, segmentPos, segments.length): _*)) + + (numbers, rest) + } + + override def compareTo(other: Version): Int = { + var diff = 0 + diff = numbers(0) - other.numbers(0) + if (diff == 0) { + diff = numbers(1) - other.numbers(1) + if (diff == 0) { + diff = numbers(2) - other.numbers(2) + if (diff == 0) { + diff = rest.compareTo(other.rest) + } + } + } + diff + } + + override def equals(o: Any): Boolean = o match { + case v: Version ⇒ compareTo(v) == 0 + case _ ⇒ false + } + + override def hashCode(): Int = { + var result = HashCode.SEED + result = HashCode.hash(result, numbers(0)) + result = HashCode.hash(result, numbers(1)) + result = HashCode.hash(result, numbers(2)) + result = HashCode.hash(result, rest) + result + } + + override def toString: String = version + } +} + +/** + * Utility that extracts [[ManifestInfo#Version]] information from META-INF/MANIFEST.MF in jar files on the classpath. + * Note that versions can only be found in ordinary jar files, for example not in "fat jars' assembled from + * many jar files. + */ +final class ManifestInfo(val system: ExtendedActorSystem) extends Extension { + import ManifestInfo._ + + /** + * Versions of artifacts from known vendors. + */ + val versions: Map[String, Version] = { + + var manifests = Map.empty[String, Version] + + try { + val resources = system.dynamicAccess.classLoader.getResources("META-INF/MANIFEST.MF") + while (resources.hasMoreElements()) { + val ios = resources.nextElement().openStream() + try { + val manifest = new Manifest(ios) + val attributes = manifest.getMainAttributes + val title = attributes.getValue(new Attributes.Name(ImplTitle)) match { + case null ⇒ attributes.getValue(new Attributes.Name(BundleName)) + case t ⇒ t + } + val version = attributes.getValue(new Attributes.Name(ImplVersion)) match { + case null ⇒ attributes.getValue(new Attributes.Name(BundleVersion)) + case v ⇒ v + } + val vendor = attributes.getValue(new Attributes.Name(ImplVendor)) match { + case null ⇒ attributes.getValue(new Attributes.Name(BundleVendor)) + case v ⇒ v + } + + if (title != null + && version != null + && vendor != null + && knownVendors(vendor)) { + manifests = manifests.updated(title, new Version(version)) + } + } finally { + ios.close() + } + } + } catch { + case ioe: IOException ⇒ + Logging(system, getClass).warning("Could not read manifest information. {}", ioe) + } + manifests + } + + /** + * Verify that the version is the same for all given artifacts. + */ + def checkSameVersion(productName: String, dependencies: immutable.Seq[String], logWarning: Boolean): Boolean = { + val filteredVersions = versions.filterKeys(dependencies.toSet) + val values = filteredVersions.values.toSet + if (values.size > 1) { + if (logWarning) { + val conflictingVersions = values.mkString(", ") + val fullInfo = filteredVersions.map { case (k, v) ⇒ s"$k:$v" }.mkString(", ") + val highestVersion = values.max + Logging(system, getClass).warning( + "Detected possible incompatible versions on the classpath. " + + s"Please note that a given $productName version MUST be the same across all modules of $productName " + + "that you are using, e.g. if you use [{}] all other modules that are released together MUST be of the " + + "same version. Make sure you're using a compatible set of libraries." + + "Possibly conflicting versions [{}] in libraries [{}]", + highestVersion, conflictingVersions, fullInfo) + } + false + } else + true + } + +} diff --git a/build.sbt b/build.sbt index e359277e61..ab7ac1d7a6 100644 --- a/build.sbt +++ b/build.sbt @@ -20,6 +20,7 @@ akka.AkkaBuild.buildSettings shellPrompt := { s => Project.extract(s).currentProject.id + " > " } resolverSettings +// When this is updated the set of modules in ActorSystem.allModules should also be updated lazy val aggregatedProjects: Seq[ProjectReference] = Seq( actor, actorTests, agent,