diff --git a/.travis.yml b/.travis.yml index c6e2d000e..08a61721f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ cache: directories: - $HOME/.ivy2/cache - $HOME/.sbt/boot + - $HOME/.jabba language: scala @@ -15,6 +16,14 @@ jdk: matrix: fast_finish: true +matrix: + include: + - env: SBT_CMD="scripted java/*" + before_install: + - curl -sL https://raw.githubusercontent.com/shyiko/jabba/0.10.1/install.sh | bash && . ~/.jabba/jabba.sh + install: + - /home/travis/.jabba/bin/jabba install openjdk@1.10 + env: global: - secure: d3bu2KNwsVHwfhbGgO+gmRfDKBJhfICdCJFGWKf2w3Gv86AJZX9nuTYRxz0KtdvEHO5Xw8WTBZLPb2thSJqhw9OCm4J8TBAVqCP0ruUj4+aqBUFy4bVexQ6WKE6nWHs4JPzPk8c6uC1LG3hMuzlC8RGETXtL/n81Ef1u7NjyXjs= @@ -26,7 +35,7 @@ env: - SBT_CMD="scripted dependency-management/*2of4" - SBT_CMD="scripted dependency-management/*3of4" - SBT_CMD="scripted dependency-management/*4of4" - - SBT_CMD="scripted java/* package/* reporter/* run/* project-load/*" + - SBT_CMD="scripted package/* reporter/* run/* project-load/*" - SBT_CMD="scripted project/*1of2" - SBT_CMD="scripted project/*2of2" - SBT_CMD="scripted source-dependencies/*1of3" diff --git a/main/src/main/contraband-scala/sbt/JavaVersion.scala b/main/src/main/contraband-scala/sbt/JavaVersion.scala new file mode 100644 index 000000000..4c630e3cd --- /dev/null +++ b/main/src/main/contraband-scala/sbt/JavaVersion.scala @@ -0,0 +1,40 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt +final class JavaVersion private ( + val numbers: Vector[Long], + val vendor: Option[String]) extends Serializable { + def numberStr: String = numbers.mkString(".") + + + override def equals(o: Any): Boolean = o match { + case x: JavaVersion => (this.numbers == x.numbers) && (this.vendor == x.vendor) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.JavaVersion".##) + numbers.##) + vendor.##) + } + override def toString: String = { + vendor.map(_ + "@").getOrElse("") + numberStr + } + private[this] def copy(numbers: Vector[Long] = numbers, vendor: Option[String] = vendor): JavaVersion = { + new JavaVersion(numbers, vendor) + } + def withNumbers(numbers: Vector[Long]): JavaVersion = { + copy(numbers = numbers) + } + def withVendor(vendor: Option[String]): JavaVersion = { + copy(vendor = vendor) + } + def withVendor(vendor: String): JavaVersion = { + copy(vendor = Option(vendor)) + } +} +object JavaVersion { + def apply(version: String): JavaVersion = sbt.internal.CrossJava.parseJavaVersion(version) + 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)) +} diff --git a/main/src/main/contraband/main.contra b/main/src/main/contraband/main.contra index eb9e9f42c..5cabb0cd4 100644 --- a/main/src/main/contraband/main.contra +++ b/main/src/main/contraband/main.contra @@ -17,3 +17,13 @@ enum PluginTrigger { AllRequirements NoTrigger } + +type JavaVersion { + numbers: [Long] + vendor: String + + #x def numberStr: String = numbers.mkString(".") + #xtostring vendor.map(_ + "@").getOrElse("") + numberStr + + #xcompanion def apply(version: String): JavaVersion = sbt.internal.CrossJava.parseJavaVersion(version) +} diff --git a/main/src/main/scala/sbt/Cross.scala b/main/src/main/scala/sbt/Cross.scala index c1257db5a..78571d78b 100644 --- a/main/src/main/scala/sbt/Cross.scala +++ b/main/src/main/scala/sbt/Cross.scala @@ -99,14 +99,24 @@ object Cross { } /** - * Parse the given command into either an aggregate command or a command for a project + * Parse the given command into a list of aggregate projects and command to issue. */ - private def parseCommand(command: String): Either[String, (String, String)] = { + private[sbt] def parseSlashCommand( + extracted: Extracted + )(command: String): (Seq[ProjectRef], String) = { + import extracted._ import DefaultParsers._ val parser = (OpOrID <~ charClass(_ == '/', "/")) ~ any.* map { - case project ~ cmd => (project, cmd.mkString) + case seg1 ~ cmd => (seg1, cmd.mkString) + } + Parser.parse(command, parser) match { + case Right((seg1, cmd)) => + structure.allProjectRefs.find(_.project == seg1) match { + case Some(proj) => (Seq(proj), cmd) + case _ => (resolveAggregates(extracted), command) + } + case _ => (resolveAggregates(extracted), command) } - Parser.parse(command, parser).left.map(_ => command) } def crossBuild: Command = @@ -115,12 +125,7 @@ object Cross { private def crossBuildCommandImpl(state: State, args: CrossArgs): State = { val x = Project.extract(state) import x._ - - val (aggs, aggCommand) = parseCommand(args.command) match { - case Right((project, cmd)) => - (structure.allProjectRefs.filter(_.project == project), cmd) - case Left(cmd) => (resolveAggregates(x), cmd) - } + val (aggs, aggCommand) = parseSlashCommand(x)(args.command) val projCrossVersions = aggs map { proj => proj -> crossVersions(x, proj) @@ -229,14 +234,11 @@ object Cross { args.command } else { args.command.map { rawCmd => - parseCommand(rawCmd) match { - case Right(_) => rawCmd // A project is specified, run as is - case Left(cmd) => - resolveAggregates(x) - .intersect(affectedRefs) - .collect { case ProjectRef(_, proj) => s"$proj/$cmd" } - .mkString("all ", " ", "") - } + val (aggs, aggCommand) = parseSlashCommand(x)(rawCmd) + aggs + .intersect(affectedRefs) + .map({ case ProjectRef(_, proj) => s"$proj/$aggCommand" }) + .mkString("all ", " ", "") } } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 4a8a34f1a..f3e550205 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -69,6 +69,7 @@ import sbt.librarymanagement.syntax._ import sbt.util.InterfaceUtil.{ toJavaFunction => f1 } import sbt.util._ import sbt.util.CacheImplicits._ +import scala.collection.immutable.ListMap import scala.concurrent.duration.FiniteDuration import scala.util.control.NonFatal import scala.xml.NodeSeq @@ -159,6 +160,9 @@ object Defaults extends BuildCommon { scalaHome :== None, apiURL := None, javaHome :== None, + discoveredJavaHomes := CrossJava.discoverJavaHomes, + javaHomes :== ListMap.empty, + fullJavaHomes := CrossJava.expandJavaHomes(discoveredJavaHomes.value ++ javaHomes.value), testForkedParallel :== false, javaOptions :== Nil, sbtPlugin :== false, diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 82191f556..bf8c9549f 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -222,6 +222,7 @@ object Keys { val scalaCompilerBridgeSource = settingKey[ModuleID]("Configures the module ID of the sources of the compiler bridge.").withRank(CSetting) val scalaArtifacts = settingKey[Seq[String]]("Configures the list of artifacts which should match the Scala binary version").withRank(CSetting) val enableBinaryCompileAnalysis = settingKey[Boolean]("Writes the analysis file in binary format") + val crossJavaVersions = settingKey[Seq[String]]("The java versions used during JDK cross testing").withRank(BPlusSetting) val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.").withRank(APlusTask) val console = taskKey[Unit]("Starts the Scala interpreter with the project classes on the classpath.").withRank(APlusTask) @@ -271,6 +272,10 @@ object Keys { val outputStrategy = settingKey[Option[sbt.OutputStrategy]]("Selects how to log output when running a main class.").withRank(DSetting) val connectInput = settingKey[Boolean]("If true, connects standard input when running a main class forked.").withRank(CSetting) val javaHome = settingKey[Option[File]]("Selects the Java installation used for compiling and forking. If None, uses the Java installation running the build.").withRank(ASetting) + val discoveredJavaHomes = settingKey[Map[String, File]]("Discovered Java home directories") + val javaHomes = settingKey[Map[String, File]]("The user-defined additional Java home directories") + val fullJavaHomes = settingKey[Map[String, File]]("Combines discoveredJavaHomes and custom javaHomes.").withRank(CTask) + val javaOptions = taskKey[Seq[String]]("Options passed to a new JVM when forking.").withRank(BPlusTask) val envVars = taskKey[Map[String, String]]("Environment variables used when forking a new JVM").withRank(BTask) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index b29cca948..d6185d41e 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -14,6 +14,7 @@ import sbt.internal.{ BuildUnit, CommandExchange, CommandStrings, + CrossJava, DefaultBackgroundJobService, EvaluateConfigurations, Inspect, @@ -190,6 +191,8 @@ object BuiltinCommands { oldLoadFailed, Cross.crossBuild, Cross.switchVersion, + CrossJava.switchJavaHome, + CrossJava.crossJavaHome, PluginCross.pluginCross, PluginCross.pluginSwitch, Cross.crossRestoreSession, diff --git a/main/src/main/scala/sbt/internal/CommandStrings.scala b/main/src/main/scala/sbt/internal/CommandStrings.scala index 99ec4637e..d30a31f29 100644 --- a/main/src/main/scala/sbt/internal/CommandStrings.scala +++ b/main/src/main/scala/sbt/internal/CommandStrings.scala @@ -415,4 +415,29 @@ $SwitchCommand [=][!] [-v] [] See also `help $CrossCommand` """ + + val JavaCrossCommand = "java+" + val JavaSwitchCommand = "java++" + + def javaCrossHelp: Help = Help.more(JavaCrossCommand, JavaCrossDetailed) + def javaSwitchHelp: Help = Help.more(JavaSwitchCommand, JavaSwitchDetailed) + + def JavaCrossDetailed = + s"""$JavaCrossCommand + Runs for each JDK version specified for cross-JDK testing. + For each string in `crossJavaVersions` in the current project, this command sets the + `javaHome` of all projects to the corresponding Java home, reloads the build, + and executes . When finished, it reloads the build with the original + `javaHome`. + Note that `Test / fork := true` is needed for `javaHome` to be effective. + See also `help $JavaSwitchCommand` +""" + + def JavaSwitchDetailed = + s"""$JavaSwitchCommand + Changes the JDK version and runs a command. + Sets the `javaHome` of all projects to and + reloads the build. If is provided, it is then executed. + See also `help $JavaSwitchCommand` +""" } diff --git a/main/src/main/scala/sbt/internal/CrossJava.scala b/main/src/main/scala/sbt/internal/CrossJava.scala new file mode 100644 index 000000000..9ee668042 --- /dev/null +++ b/main/src/main/scala/sbt/internal/CrossJava.scala @@ -0,0 +1,365 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt +package internal + +import java.io.File +import scala.collection.immutable.ListMap +import sbt.io.Path +import sbt.io.syntax._ +import sbt.Cross._ +import sbt.Def.{ ScopedKey, Setting } +import sbt.internal.util.complete.DefaultParsers._ +import sbt.internal.util.AttributeKey +import sbt.internal.util.complete.{ DefaultParsers, Parser } +import sbt.internal.CommandStrings.{ + JavaCrossCommand, + JavaSwitchCommand, + javaCrossHelp, + javaSwitchHelp +} + +private[sbt] object CrossJava { + // parses jabaa style version number adopt@1.8 + def parseJavaVersion(version: String): JavaVersion = { + def splitDot(s: String): Vector[Long] = + Option(s) match { + case Some(x) => x.split('.').toVector.filterNot(_ == "").map(_.toLong) + case _ => Vector() + } + def splitAt(s: String): Vector[String] = + Option(s) match { + case Some(x) => x.split('@').toVector + case _ => Vector() + } + splitAt(version) match { + case Vector(vendor, rest) => JavaVersion(splitDot(rest), Option(vendor)) + case Vector(rest) => JavaVersion(splitDot(rest), None) + case _ => sys.error(s"Invalid JavaVersion: $version") + } + } + + def lookupJavaHome(jv: String, mappings: Map[String, File]): File = { + val ms = mappings map { case (k, v) => (JavaVersion(k), v) } + lookupJavaHome(JavaVersion(jv), ms) + } + + def lookupJavaHome(jv: JavaVersion, mappings: Map[JavaVersion, File]): File = { + mappings.get(jv) match { + case Some(dir) => dir + + // when looking for "10" it should match "openjdk@10" + case None if jv.vendor.isEmpty => + val noVendors: Map[JavaVersion, File] = mappings map { + case (k, v) => k.withVendor(None) -> v + } + noVendors.get(jv).getOrElse(javaHomeNotFound(jv, mappings)) + case _ => javaHomeNotFound(jv, mappings) + } + } + + private def javaHomeNotFound(version: JavaVersion, mappings: Map[JavaVersion, File]): Nothing = { + sys.error(s"""Java home for $version was not found in $mappings + | + |use Global / javaHomes += JavaVersion("$version") -> file(...)""".stripMargin) + } + + private case class SwitchTarget(version: Option[JavaVersion], home: Option[File], force: Boolean) + private case class SwitchJavaHome(target: SwitchTarget, verbose: Boolean, command: Option[String]) + + private def switchParser(state: State): Parser[SwitchJavaHome] = { + import DefaultParsers._ + def versionAndCommand(spacePresent: Boolean) = { + val x = Project.extract(state) + import x._ + val javaHomes = getJavaHomesTyped(x, currentRef) + val knownVersions = javaHomes.keysIterator.map(_.numberStr).toVector + val version: Parser[SwitchTarget] = + (token( + (StringBasic <~ "@").? ~ ((NatBasic) ~ ("." ~> NatBasic).*) + .examples(knownVersions: _*) ~ "!".? + ) || token(StringBasic)) + .map { + case Left(((vendor, (v1, vs)), bang)) => + val force = bang.isDefined + val versionArg = (Vector(v1) ++ vs) map { _.toLong } + SwitchTarget(Option(JavaVersion(versionArg, vendor)), None, force) + case Right(home) => + SwitchTarget(None, Option(new File(home)), true) + } + val spacedVersion = + if (spacePresent) version + else version & spacedFirst(JavaSwitchCommand) + val verbose = Parser.opt(token(Space ~> "-v")) + val optionalCommand = Parser.opt(token(Space ~> matched(state.combinedParser))) + (spacedVersion ~ verbose ~ optionalCommand).map { + case v ~ verbose ~ command => + SwitchJavaHome(v, verbose.isDefined, command) + } + } + token(JavaSwitchCommand ~> OptSpace) flatMap { sp => + versionAndCommand(sp.nonEmpty) + } + } + + private def getJavaHomes( + extracted: Extracted, + proj: ResolvedReference + ): Map[String, File] = { + import extracted._ + (Keys.fullJavaHomes in proj get structure.data).get + } + + private def getJavaHomesTyped( + extracted: Extracted, + proj: ResolvedReference + ): Map[JavaVersion, File] = { + getJavaHomes(extracted, proj) map { case (k, v) => (JavaVersion(k), v) } + } + + private def getCrossJavaVersions( + extracted: Extracted, + proj: ResolvedReference + ): Seq[String] = { + import extracted._ + import Keys._ + (crossJavaVersions in proj get structure.data).getOrElse(Nil) + } + + private def getCrossJavaHomes(extracted: Extracted, proj: ResolvedReference): Seq[File] = { + import extracted._ + import Keys._ + val fjh = (fullJavaHomes in proj get structure.data).get + (crossJavaVersions in proj get structure.data) map { jvs => + jvs map { jv => + lookupJavaHome(jv, fjh) + } + } getOrElse Vector() + } + + private def switchCommandImpl(state: State, switch: SwitchJavaHome): State = { + val extracted = Project.extract(state) + import extracted._ + import Keys.javaHome + + // filter out subprojects based on switch target e.g. "10" vs what's in crossJavaVersions + // for the subproject. Only if crossJavaVersions is non-empty, and does NOT include "10" + // it will skip the subproject. + val projects: Seq[(ResolvedReference, Seq[String])] = { + val projectJavaVersions = + structure.allProjectRefs.map(proj => proj -> getCrossJavaVersions(extracted, proj)) + if (switch.target.force) projectJavaVersions + else + switch.target.version match { + case None => projectJavaVersions + case Some(v) => + projectJavaVersions flatMap { + case (proj, versions) => + if (versions.isEmpty || versions.contains(v)) Vector(proj -> versions) + else Vector() + } + } + } + + def setJavaHomeForProjects: State = { + val newSettings = projects.flatMap { + case (proj, javaVersions) => + val fjh = getJavaHomesTyped(extracted, proj) + val home = switch.target match { + case SwitchTarget(Some(v), _, _) => lookupJavaHome(v, fjh) + case SwitchTarget(_, Some(h), _) => h + case _ => sys.error(s"unexpected ${switch.target}") + } + val scope = Scope(Select(proj), Zero, Zero, Zero) + Seq( + javaHome in scope := Some(home) + ) + } + + val filterKeys: Set[AttributeKey[_]] = Set(javaHome).map(_.key) + + val projectsContains: Reference => Boolean = projects.map(_._1).toSet.contains + + // Filter out any old javaHome version settings that were added, this is just for hygiene. + val filteredRawAppend = session.rawAppend.filter(_.key match { + case ScopedKey(Scope(Select(ref), Zero, Zero, Zero), key) + if filterKeys.contains(key) && projectsContains(ref) => + false + case _ => true + }) + + val newSession = session.copy(rawAppend = filteredRawAppend ++ newSettings) + + BuiltinCommands.reapply(newSession, structure, state) + } + + setJavaHomeForProjects + } + + def switchJavaHome: Command = + Command.arb(requireSession(switchParser), javaSwitchHelp)(switchCommandImpl) + + def crossJavaHome: Command = + Command.arb(requireSession(crossParser), javaCrossHelp)(crossJavaHomeCommandImpl) + + private case class CrossArgs(command: String, verbose: Boolean) + + /** + * Parse the given command into either an aggregate command or a command for a project + */ + private def crossParser(state: State): Parser[CrossArgs] = + token(JavaCrossCommand <~ OptSpace) flatMap { _ => + (token(Parser.opt("-v" <~ Space)) ~ token(matched(state.combinedParser))).map { + case (verbose, command) => CrossArgs(command, verbose.isDefined) + } & spacedFirst(JavaCrossCommand) + } + + private def crossJavaHomeCommandImpl(state: State, args: CrossArgs): State = { + val x = Project.extract(state) + import x._ + val (aggs, aggCommand) = Cross.parseSlashCommand(x)(args.command) + val projCrossVersions = aggs map { proj => + proj -> getCrossJavaHomes(x, proj) + } + // if we support javaHome, projVersions should be cached somewhere since + // running ++2.11.1 is at the root level is going to mess with the scalaVersion for the aggregated subproj + val projVersions = (projCrossVersions flatMap { + case (proj, versions) => versions map { proj.project -> _ } + }).toList + + val verbose = "" + // println(s"projVersions $projVersions") + + if (projVersions.isEmpty) { + state + } else { + // Detect whether a task or command has been issued + val allCommands = Parser.parse(aggCommand, Act.aggregatedKeyParser(x)) match { + case Left(_) => + // It's definitely not a task, check if it's a valid command, because we don't want to emit the warning + // message below for typos. + val validCommand = Parser.parse(aggCommand, state.combinedParser).isRight + + val distinctCrossConfigs = projCrossVersions.map(_._2.toSet).distinct + if (validCommand && distinctCrossConfigs.size > 1) { + state.log.warn( + "Issuing a Java cross building command, but not all sub projects have the same cross build " + + "configuration. This could result in subprojects cross building against Java versions that they are " + + "not compatible with. Try issuing cross building command with tasks instead, since sbt will be able " + + "to ensure that cross building is only done using configured project and Java version combinations " + + "that are configured." + ) + state.log.debug("Java versions configuration is:") + projCrossVersions.foreach { + case (project, versions) => state.log.debug(s"$project: $versions") + } + } + + // Execute using a blanket switch + projCrossVersions.toMap.apply(currentRef).flatMap { version => + // Force scala version + Seq(s"$JavaSwitchCommand $verbose $version!", aggCommand) + } + + case Right(_) => + // We have a key, we're likely to be able to cross build this using the per project behaviour. + + // Group all the projects by scala version + projVersions.groupBy(_._2).mapValues(_.map(_._1)).toSeq.flatMap { + case (version, Seq(project)) => + // If only one project for a version, issue it directly + Seq(s"$JavaSwitchCommand $verbose $version", s"$project/$aggCommand") + case (version, projects) if aggCommand.contains(" ") => + // If the command contains a space, then the `all` command won't work because it doesn't support issuing + // commands with spaces, so revert to running the command on each project one at a time + s"$JavaSwitchCommand $verbose $version" :: projects + .map(project => s"$project/$aggCommand") + case (version, projects) => + // First switch scala version, then use the all command to run the command on each project concurrently + Seq( + s"$JavaSwitchCommand $verbose $version", + projects.map(_ + "/" + aggCommand).mkString("all ", " ", "") + ) + } + } + + allCommands.toList ::: captureCurrentSession(state, x) + } + } + + private val JavaCapturedSession = AttributeKey[Seq[Setting[_]]]("javaCrossCapturedSession") + + private def captureCurrentSession(state: State, extracted: Extracted): State = { + state.put(JavaCapturedSession, extracted.session.rawAppend) + } + + def discoverJavaHomes: ListMap[String, File] = { + import JavaDiscoverConfig._ + val configs = Vector(jabba, linux, macOS) + ListMap(configs flatMap { _.javaHomes }: _*) + } + + sealed trait JavaDiscoverConf { + def javaHomes: Vector[(String, File)] + } + + object JavaDiscoverConfig { + val linux = new JavaDiscoverConf { + val base: File = file("/usr") / "lib" / "jvm" + val JavaHomeDir = """java-([0-9]+)-.*""".r + def javaHomes: Vector[(String, File)] = + wrapNull(base.list()).collect { + case dir @ JavaHomeDir(ver) => JavaVersion(ver).toString -> (base / dir) + } + } + + val macOS = new JavaDiscoverConf { + val base: File = file("/Library") / "Java" / "JavaVirtualMachines" + val JavaHomeDir = """jdk-?(1\.)?([0-9]+).*""".r + def javaHomes: Vector[(String, File)] = + wrapNull(base.list()).collect { + case dir @ JavaHomeDir(m, n) => + JavaVersion(nullBlank(m) + n).toString -> (base / dir / "Contents" / "Home") + } + } + + // See https://github.com/shyiko/jabba + val jabba = new JavaDiscoverConf { + val base: File = Path.userHome / ".jabba" / "jdk" + val JavaHomeDir = """([\w\-]+)\@(1\.)?([0-9]+).*""".r + def javaHomes: Vector[(String, File)] = + wrapNull(base.list()).collect { + case dir @ JavaHomeDir(vendor, m, n) => + val v = JavaVersion(nullBlank(m) + n).withVendor(vendor).toString + if ((base / dir / "Contents" / "Home").exists) v -> (base / dir / "Contents" / "Home") + else v -> (base / dir) + } + } + } + + def nullBlank(s: String): String = + 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 { + case (k, v) => + val jv = JavaVersion(k) + if (oneDot.contains(jv.numbers)) + Vector(k -> v, jv.withNumbers(oneDot(jv.numbers)).toString -> v) + else Vector(k -> v) + } + + def wrapNull(a: Array[String]): Vector[String] = + if (a eq null) Vector() + else a.toVector +} diff --git a/sbt/src/sbt-test/java/cross/A.scala b/sbt/src/sbt-test/java/cross/A.scala new file mode 100644 index 000000000..e9ff1f72a --- /dev/null +++ b/sbt/src/sbt-test/java/cross/A.scala @@ -0,0 +1,16 @@ +package pkg + +import java.nio.file.{ Paths, Files } +import java.nio.charset.Charset + +object A extends App { + val out = Paths.get("out.txt") + val content = sys.props("java.version") + val w = Files.newBufferedWriter(out, Charset.forName("UTF-8")) + try { + w.write(content) + w.flush() + } finally { + w.close + } +} diff --git a/sbt/src/sbt-test/java/cross/build.sbt b/sbt/src/sbt-test/java/cross/build.sbt new file mode 100644 index 000000000..c71bd179e --- /dev/null +++ b/sbt/src/sbt-test/java/cross/build.sbt @@ -0,0 +1,25 @@ +import complete.DefaultParsers._ + +val check = inputKey[Unit]("Runs the check") + +lazy val root = (project in file(".")) + .settings( + ThisBuild / scalaVersion := "2.12.6", + crossJavaVersions := List("1.8"), + + // read out.txt and see if it starts with the passed in number + check := { + val arg1: Int = (Space ~> NatBasic).parsed + file("out.txt") match { + case out if out.exists => + IO.readLines(out).headOption match { + case Some(v) if v startsWith arg1.toString => () + case Some(v) if v startsWith s"1.$arg1" => () + case x => sys.error(s"unexpected value: $x") + } + case out => sys.error(s"$out doesn't exist") + } + }, + + Compile / run / fork := true, + ) diff --git a/sbt/src/sbt-test/java/cross/test b/sbt/src/sbt-test/java/cross/test new file mode 100644 index 000000000..ff76c51bf --- /dev/null +++ b/sbt/src/sbt-test/java/cross/test @@ -0,0 +1,6 @@ +> java+ run +> check 8 + +> java++ 10! +> run +> check 10 diff --git a/sbt/src/sbt-test/java/home-discovery/build.sbt b/sbt/src/sbt-test/java/home-discovery/build.sbt new file mode 100644 index 000000000..7177ca692 --- /dev/null +++ b/sbt/src/sbt-test/java/home-discovery/build.sbt @@ -0,0 +1,5 @@ +Global / javaHomes += "6" -> file("/good/old/times/java-6") + +TaskKey[Unit]("check") := { + assert(fullJavaHomes.value("1.6").getAbsolutePath.contains("java-6")) +} diff --git a/sbt/src/sbt-test/java/home-discovery/test b/sbt/src/sbt-test/java/home-discovery/test new file mode 100644 index 000000000..15675b169 --- /dev/null +++ b/sbt/src/sbt-test/java/home-discovery/test @@ -0,0 +1 @@ +> check diff --git a/scripted/sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala b/scripted/sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala index dabaeeca1..77e3b7467 100644 --- a/scripted/sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala +++ b/scripted/sbt/src/main/scala/sbt/scriptedtest/ScriptedTests.scala @@ -206,6 +206,7 @@ final class ScriptedTests( case "dependency-management/update-sbt-classifiers" => LauncherBased // tbd case "dependency-management/url" => LauncherBased // tbd case "java/argfile" => LauncherBased // sbt/Package$ + case "java/cross" => LauncherBased // sbt/Package$ case "java/basic" => LauncherBased // sbt/Package$ case "java/varargs-main" => LauncherBased // sbt/Package$ case "package/lazy-name" => LauncherBased // sbt/Package$