diff --git a/main/src/main/contraband-scala/sbt/JavaVersion.scala b/main/src/main/contraband-scala/sbt/JavaVersion.scala index 4c630e3cd..7fe21dbc2 100644 --- a/main/src/main/contraband-scala/sbt/JavaVersion.scala +++ b/main/src/main/contraband-scala/sbt/JavaVersion.scala @@ -6,26 +6,32 @@ package sbt final class JavaVersion private ( val numbers: Vector[Long], + val tags: Vector[String], val vendor: Option[String]) extends Serializable { def numberStr: String = numbers.mkString(".") - + private def tagStr: String = if (tags.isEmpty) "" else tags.mkString("-", "-", "") + private def this() = this(Vector(), Vector(), None) + private def this(numbers: Vector[Long], vendor: Option[String]) = this(numbers, Vector(), vendor) override def equals(o: Any): Boolean = o match { - case x: JavaVersion => (this.numbers == x.numbers) && (this.vendor == x.vendor) + case x: JavaVersion => (this.numbers == x.numbers) && (this.tags == x.tags) && (this.vendor == x.vendor) case _ => false } override def hashCode: Int = { - 37 * (37 * (37 * (17 + "sbt.JavaVersion".##) + numbers.##) + vendor.##) + 37 * (37 * (37 * (37 * (17 + "sbt.JavaVersion".##) + numbers.##) + tags.##) + vendor.##) } override def toString: String = { - vendor.map(_ + "@").getOrElse("") + numberStr + vendor.map(_ + "@").getOrElse("") + numberStr + tagStr } - private[this] def copy(numbers: Vector[Long] = numbers, vendor: Option[String] = vendor): JavaVersion = { - new JavaVersion(numbers, vendor) + private[this] def copy(numbers: Vector[Long] = numbers, tags: Vector[String] = tags, vendor: Option[String] = vendor): JavaVersion = { + new JavaVersion(numbers, tags, vendor) } def withNumbers(numbers: Vector[Long]): JavaVersion = { copy(numbers = numbers) } + def withTags(tags: Vector[String]): JavaVersion = { + copy(tags = tags) + } def withVendor(vendor: Option[String]): JavaVersion = { copy(vendor = vendor) } @@ -35,6 +41,9 @@ final class JavaVersion private ( } object JavaVersion { def apply(version: String): JavaVersion = sbt.internal.CrossJava.parseJavaVersion(version) + def apply(): JavaVersion = new JavaVersion() def apply(numbers: Vector[Long], vendor: Option[String]): JavaVersion = new JavaVersion(numbers, vendor) def apply(numbers: Vector[Long], vendor: String): JavaVersion = new JavaVersion(numbers, Option(vendor)) + def apply(numbers: Vector[Long], tags: Vector[String], vendor: Option[String]): JavaVersion = new JavaVersion(numbers, tags, vendor) + def apply(numbers: Vector[Long], tags: Vector[String], vendor: String): JavaVersion = new JavaVersion(numbers, tags, Option(vendor)) } diff --git a/main/src/main/contraband/main.contra b/main/src/main/contraband/main.contra index 5cabb0cd4..2c0e64739 100644 --- a/main/src/main/contraband/main.contra +++ b/main/src/main/contraband/main.contra @@ -19,11 +19,13 @@ enum PluginTrigger { } type JavaVersion { - numbers: [Long] - vendor: String + numbers: [Long] @since("1.2.0") + tags: [String] @since("1.3.0") + vendor: String @since("1.2.0") #x def numberStr: String = numbers.mkString(".") - #xtostring vendor.map(_ + "@").getOrElse("") + numberStr + #x private def tagStr: String = if (tags.isEmpty) "" else tags.mkString("-", "-", "") + #xtostring vendor.map(_ + "@").getOrElse("") + numberStr + tagStr #xcompanion def apply(version: String): JavaVersion = sbt.internal.CrossJava.parseJavaVersion(version) } diff --git a/main/src/main/scala/sbt/internal/CrossJava.scala b/main/src/main/scala/sbt/internal/CrossJava.scala index 04b4ac8eb..eea281007 100644 --- a/main/src/main/scala/sbt/internal/CrossJava.scala +++ b/main/src/main/scala/sbt/internal/CrossJava.scala @@ -45,6 +45,49 @@ private[sbt] object CrossJava { } } + def parseSdkmanString(version: String): JavaVersion = { + val Num = """([0-9]+)""".r + def splitDash(str: String): Vector[String] = + Option(str) match { + case Some(x) => x.split('-').toVector + case _ => Vector() + } + def splitDot(str: String): Vector[String] = + Option(str) match { + case Some(x) => x.split('.').toVector.filterNot(_ == "") + case _ => Vector() + } + splitDash(version) match { + case xs if xs.size < 2 => sys.error(s"Invalid SDKMAN Java version: $version") + case xs => + val ds = splitDot(xs.init.head) + val nums = ds.takeWhile( + _ match { + case Num(_) => true + case _ => false + } + ) map { _.toLong } + val nonNum = ds.drop(nums.size).mkString("") + // last dash indicates vendor code + val (vnd0, tag0) = (xs.last, nonNum) match { + case ("adpt", "hs") => ("adpt", "") + case ("adpt", "j9") => ("adopt-openj9", "") + case (v, t) => (v, t) + } + val vnd = vnd0 match { + case "adpt" => "adopt" + case "open" => "openjdk" + case "grl" => "graalvm" + case "amzn" => "corretto" + case _ => vnd0 + } + val tag1: String = xs.init.tail.mkString("") + val tags = (if (tag0 == "") Vector.empty[String] else Vector(tag0)) ++ + (if (tag1 == "") Vector.empty[String] else Vector(tag1)) + JavaVersion().withNumbers(nums).withVendor(vnd).withTags(tags) + } + } + def lookupJavaHome(jv: String, mappings: Map[String, File]): File = { val ms = mappings map { case (k, v) => (JavaVersion(k), v) } lookupJavaHome(JavaVersion(jv), ms) @@ -377,6 +420,18 @@ private[sbt] object CrossJava { } } + class SdkmanDiscoverConfig extends JavaDiscoverConf { + val base: File = Path.userHome / ".sdkman" / "candidates" / "java" + def candidates(): Vector[String] = wrapNull(base.list()) + def javaHomes: Vector[(String, File)] = + candidates + .collect { + case dir if dir.contains("-") => + val v = CrossJava.parseSdkmanString(dir).toString + v -> (base / dir) + } + } + class WindowsDiscoverConfig(base: File) extends JavaDiscoverConf { def candidates() = wrapNull(base.list()) @@ -408,6 +463,7 @@ private[sbt] object CrossJava { val configs = Vector( new JabbaDiscoverConfig, + new SdkmanDiscoverConfig, new LinuxDiscoverConfig(file("/usr") / "java"), new LinuxDiscoverConfig(file("/usr") / "lib" / "jvm"), new MacOsDiscoverConfig, @@ -421,18 +477,34 @@ private[sbt] object CrossJava { if (s eq null) "" else s - // expand Java versions to 1-20 to 1.x, and vice versa to accept both "1.8" and "8" - private val oneDot = Map((1L to 20L).toVector flatMap { i => - Vector(Vector(i) -> Vector(1L, i), Vector(1L, i) -> Vector(i)) - }: _*) - def expandJavaHomes(hs: Map[String, File]): Map[String, File] = - hs flatMap { + def expandJavaHomes(hs: Map[String, File]): Map[String, File] = { + val parsed = hs map { + case (k, v) => JavaVersion(k) -> v + } + // first ignore vnd + val withAndWithoutVnd = parsed flatMap { case (k, v) => - val jv = JavaVersion(k) - if (oneDot.contains(jv.numbers)) - Vector(k -> v, jv.withNumbers(oneDot(jv.numbers)).toString -> v) + if (k.vendor.isDefined) Vector(k -> v, k.withVendor(None) -> v) else Vector(k -> v) } + val normalizeNumbers = withAndWithoutVnd flatMap { + case (k, v) => + k.numbers match { + case Vector(1L, minor, _*) => + Vector(k -> v, k.withNumbers(Vector(minor)) -> v) + case Vector(major) if major > 1 => + Vector(k -> v, k.withNumbers(Vector(1L, major)) -> v) + case Vector(major, minor, _*) if major > 1 => + Vector(k -> v, k.withNumbers(Vector(major)) -> v, k.withNumbers(Vector(1L, major)) -> v) + case _ => + Vector(k -> v) + } + } + val result: Map[String, File] = normalizeNumbers map { + case (k, v) => (k.toString -> v) + } + result + } def wrapNull(a: Array[String]): Vector[String] = if (a eq null) Vector() diff --git a/main/src/test/scala/sbt/internal/CrossJavaTest.scala b/main/src/test/scala/sbt/internal/CrossJavaTest.scala index a3d617ce0..2a9aba62b 100644 --- a/main/src/test/scala/sbt/internal/CrossJavaTest.scala +++ b/main/src/test/scala/sbt/internal/CrossJavaTest.scala @@ -5,25 +5,26 @@ * Licensed under Apache License 2.0 (see LICENSE) */ -package sbt.internal +package sbt +package internal -import org.specs2.mutable.Specification +import org.scalatest._ import sbt.internal.CrossJava.JavaDiscoverConfig._ +import scala.collection.immutable.ListMap -class CrossJavaTest extends Specification { - "The Java home selector" should { - "select the most recent" in { +class CrossJavaTest extends FunSuite with DiagrammedAssertions { + test("The Java home selector should select the most recent") { + assert( List("jdk1.8.0.jdk", "jdk1.8.0_121.jdk", "jdk1.8.0_45.jdk") .sortWith(CrossJava.versionOrder) - .last must be equalTo ("jdk1.8.0_121.jdk") - } + .last == "jdk1.8.0_121.jdk" + ) } - "The Linux Java home selector" should { - "correctly pick up fedora java installations" in { - val conf = new LinuxDiscoverConfig(sbt.io.syntax.file(".")) { - override def candidates(): Vector[String] = - """ + test("The Linux Java home selector should correctly pick up Fedora Java installations") { + val conf = new LinuxDiscoverConfig(sbt.io.syntax.file(".")) { + override def candidates(): Vector[String] = + """ |java-1.8.0-openjdk-1.8.0.162-3.b12.fc28.x86_64 |java-1.8.0-openjdk-1.8.0.172-9.b11.fc28.x86_64 |java-1.8.0 @@ -34,49 +35,106 @@ class CrossJavaTest extends Specification { |jre-1.8.0-openjdk-1.8.0.172-9.b11.fc28.x86_64 |jre-openjdk """.stripMargin.split("\n").filter(_.nonEmpty).toVector - } - val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last - version must be equalTo ("1.8") - file.getName must be equalTo ("java-1.8.0-openjdk-1.8.0.172-9.b11.fc28.x86_64") - } - - "correctly pick up Oracle RPM installations" in { - val conf = new LinuxDiscoverConfig(sbt.io.syntax.file(".")) { - override def candidates(): Vector[String] = Vector("jdk1.8.0_172-amd64") - } - val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last - version must be equalTo ("1.8") - file.getName must be equalTo ("jdk1.8.0_172-amd64") } + val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last + assert(version == "1.8") + assert(file.getName == "java-1.8.0-openjdk-1.8.0.172-9.b11.fc28.x86_64") } - "The Windows Java home selector" should { - "correctly pick up a JDK" in { - val conf = new WindowsDiscoverConfig(sbt.io.syntax.file(".")) { - override def candidates() = Vector("jdk1.7.0") - } - val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last - version must equalTo("1.7") - file.getName must be equalTo ("jdk1.7.0") + test("The Linux Java home selector should correctly pick up Oracle RPM installations") { + val conf = new LinuxDiscoverConfig(sbt.io.syntax.file(".")) { + override def candidates(): Vector[String] = Vector("jdk1.8.0_172-amd64") } + val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last + assert(version == "1.8") + assert(file.getName == "jdk1.8.0_172-amd64") } - "The JAVA_HOME selector" should { - "correctly pick up a JDK" in { - val conf = new JavaHomeDiscoverConfig { - override def home() = Some("/opt/jdk8") - } - val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last - version must equalTo("8") - file.getName must be equalTo ("jdk8") + test("The Windows Java home selector should correctly pick up a JDK") { + val conf = new WindowsDiscoverConfig(sbt.io.syntax.file(".")) { + override def candidates() = Vector("jdk1.7.0") } - "correctly pick up an Oracle JDK" in { - val conf = new JavaHomeDiscoverConfig { - override def home() = Some("/opt/oracle-jdk-bin-1.8.0.181") - } - val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last - version must equalTo("1.8") - file.getName must be equalTo ("oracle-jdk-bin-1.8.0.181") + val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last + assert(version == "1.7") + assert(file.getName == "jdk1.7.0") + } + + test("The JAVA_HOME selector should correctly pick up a JDK") { + val conf = new JavaHomeDiscoverConfig { + override def home() = Some("/opt/jdk8") } + val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last + assert(version == "8") + assert(file.getName == "jdk8") + } + + test("The JAVA_HOME selector should correctly pick up an Oracle JDK") { + val conf = new JavaHomeDiscoverConfig { + override def home() = Some("/opt/oracle-jdk-bin-1.8.0.181") + } + val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last + assert(version == "1.8") + assert(file.getName == "oracle-jdk-bin-1.8.0.181") + } + + test("The SDKMAN selector should correctly pick up an AdoptOpenJDK") { + val conf = new SdkmanDiscoverConfig { + override def candidates() = Vector("11.0.2.hs-adpt") + } + val (version, file) = conf.javaHomes.sortWith(CrossJava.versionOrder).last + assert(version == "adopt@11.0.2") + assert(file.getName == "11.0.2.hs-adpt") + } + + test("expandJavaHomes") { + val conf = new SdkmanDiscoverConfig { + override def candidates() = Vector("11.0.2.hs-adpt") + } + val hs = CrossJava.expandJavaHomes(ListMap(conf.javaHomes: _*)) + assert(hs.contains("11")) + } + + test("SDKMAN candidate parsing") { + assert( + CrossJava + .parseSdkmanString("11.0.2.hs-adpt") == JavaVersion(Vector(11L, 0L, 2L), Some("adopt")) + ) + assert( + CrossJava + .parseSdkmanString("11.0.2.j9-adpt") == JavaVersion( + Vector(11L, 0L, 2L), + Some("adopt-openj9") + ) + ) + assert( + CrossJava + .parseSdkmanString("13.ea.13-open") == JavaVersion( + Vector(13L), + Vector("ea13"), + Some("openjdk") + ) + ) + assert( + CrossJava + .parseSdkmanString("12.0.0-zulu") == JavaVersion( + Vector(12L, 0L, 0L), + Some("zulu") + ) + ) + assert( + CrossJava + .parseSdkmanString("8.0.201-oracle") == JavaVersion( + Vector(8L, 0L, 201L), + Some("oracle") + ) + ) + assert( + CrossJava + .parseSdkmanString("1.0.0-rc-14-grl") == JavaVersion( + Vector(1L, 0L, 0L), + Vector("rc14"), + Some("graalvm") + ) + ) } }