Merge branch 'develop' into patch-1

This commit is contained in:
Conny Brunnkvist 2021-09-21 08:52:43 +07:00 committed by GitHub
commit eeef5eb7d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 148 additions and 598 deletions

View File

@ -10,37 +10,37 @@ jobs:
matrix:
include:
- os: ubuntu-latest
java: 11
java: "17.0-custom=tgz+https://download.java.net/java/GA/jdk17/0d483333a00540d886896bac774ff48b/35/GPL/openjdk-17_linux-x64_bin.tar.gz"
jobtype: 1
- os: ubuntu-latest
java: 11
java: "17.0-custom=tgz+https://download.java.net/java/GA/jdk17/0d483333a00540d886896bac774ff48b/35/GPL/openjdk-17_linux-x64_bin.tar.gz"
jobtype: 2
- os: ubuntu-latest
java: 11
java: "17.0-custom=tgz+https://download.java.net/java/GA/jdk17/0d483333a00540d886896bac774ff48b/35/GPL/openjdk-17_linux-x64_bin.tar.gz"
jobtype: 3
- os: ubuntu-latest
java: 11
java: "17.0-custom=tgz+https://download.java.net/java/GA/jdk17/0d483333a00540d886896bac774ff48b/35/GPL/openjdk-17_linux-x64_bin.tar.gz"
jobtype: 4
- os: ubuntu-latest
java: 11
java: "17-custom=tgz+https://download.java.net/java/GA/jdk17/0d483333a00540d886896bac774ff48b/35/GPL/openjdk-17_linux-x64_bin.tar.gz"
jobtype: 5
- os: ubuntu-latest
java: 8
java: "adopt@1.8"
jobtype: 6
- os: ubuntu-latest
java: 8
java: "adopt@1.8"
jobtype: 7
- os: macos-latest
java: 8
java: "adopt@1.8"
jobtype: 8
- os: windows-latest
java: 8
java: "adopt@1.8"
jobtype: 9
runs-on: ${{ matrix.os }}
env:
JAVA_OPTS: -Xms800M -Xmx2G -Xss6M -XX:ReservedCodeCacheSize=128M -server -Dsbt.io.virtual=false -Dfile.encoding=UTF-8
JVM_OPTS: -Xms800M -Xmx2G -Xss6M -XX:ReservedCodeCacheSize=128M -server -Dsbt.io.virtual=false -Dfile.encoding=UTF-8
SCALA_212: 2.12.14
SCALA_212: 2.12.15
SCALA_213: 2.13.6
UTIL_TESTS: "utilCache/test utilControl/test utilInterface/test utilLogging/test utilPosition/test utilRelation/test utilScripted/test utilTracking/test"
SBT_LOCAL: false
@ -72,7 +72,7 @@ jobs:
- name: Setup
uses: olafurpg/setup-scala@v13
with:
java-version: "adopt@1.${{ matrix.java }}"
java-version: "${{ matrix.java }}"
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:

View File

@ -131,6 +131,10 @@ Listening for transport dt_socket at address: 5005
Please note that this alternative launcher does _not_ have feature parity with sbt/launcher. (Meta)
contributions welcome! :-D
### Updating Scala version
See https://github.com/sbt/sbt/pull/6522 for the list of files to change for Scala version upgrade.
### Diagnosing build failures
Globally included plugins can interfere building `sbt`; if you are getting errors building sbt, try disabling all globally included plugins and try again.

View File

@ -10,7 +10,7 @@ import scala.util.Try
// ThisBuild settings take lower precedence,
// but can be shared across the multi projects.
ThisBuild / version := {
val v = "1.5.5-SNAPSHOT"
val v = "1.6.0-SNAPSHOT"
nightlyVersion.getOrElse(v)
}
ThisBuild / version2_13 := "2.0.0-SNAPSHOT"
@ -45,7 +45,8 @@ ThisBuild / scmInfo := Some(
ThisBuild / resolvers += Resolver.mavenLocal
Global / semanticdbEnabled := !(Global / insideCI).value
Global / semanticdbVersion := "4.4.20"
// Change main/src/main/scala/sbt/plugins/SemanticdbPlugin.scala too, if you change this.
Global / semanticdbVersion := "4.4.28"
val excludeLint = SettingKey[Set[Def.KeyedInitialize[_]]]("excludeLintKeys")
Global / excludeLint := (Global / excludeLint).?.value.getOrElse(Set.empty)
Global / excludeLint += componentID
@ -597,6 +598,9 @@ lazy val runProj = (project in file("run"))
exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#CustomOutput.copy$default$*"),
exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#LoggedOutput.copy"),
exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#LoggedOutput.copy$default$*"),
exclude[Problem]("sbt.TrapExit*"),
exclude[MissingClassProblem]("sbt.ExitCode"), // private
exclude[MissingClassProblem]("sbt.LoggingExceptionHandler"), // private
)
)
.configure(addSbtIO, addSbtCompilerClasspath)

View File

@ -26,7 +26,7 @@ lazy val sbtVersionToRelease = sys.props.getOrElse("sbt.build.version", sys.env.
}))
lazy val scala210 = "2.10.7"
lazy val scala212 = "2.12.14"
lazy val scala212 = "2.12.15"
lazy val scala210Jline = "org.scala-lang" % "jline" % scala210
lazy val jansi = {
if (sbtVersionToRelease startsWith "1.") "org.fusesource.jansi" % "jansi" % "1.12"

View File

@ -6,7 +6,7 @@ import java.io.File
object SbtRunnerTest extends SimpleTestSuite with PowerAssertions {
// 1.3.0, 1.3.0-M4
private val versionRegEx = "\\d(\\.\\d+){2}(-\\w+)?"
private[test] val versionRegEx = "\\d(\\.\\d+){2}(-\\w+)?"
lazy val isWindows: Boolean = sys.props("os.name").toLowerCase(java.util.Locale.ENGLISH).contains("windows")
lazy val sbtScript =
@ -49,13 +49,6 @@ object SbtRunnerTest extends SimpleTestSuite with PowerAssertions {
()
}
test("sbt --script-version should print sbtVersion") {
val out = sbtProcess("--script-version").!!.trim
val expectedVersion = "^"+versionRegEx+"$"
assert(out.matches(expectedVersion))
()
}
test("sbt --sbt-jar should run") {
val out = sbtProcess("compile", "-v", "--sbt-jar", "../target/universal/stage/bin/sbt-launch.jar").!!.linesIterator.toList
assert(out.contains[String]("../target/universal/stage/bin/sbt-launch.jar") ||

View File

@ -109,7 +109,6 @@ object SbtScriptTest extends SimpleTestSuite with PowerAssertions {
assert(out.contains[String]("-Xss6M"))
}
makeTest(
name = "sbt with -Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080 in SBT_OPTS",
sbtOpts = "-Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080",
@ -169,4 +168,15 @@ object SbtScriptTest extends SimpleTestSuite with PowerAssertions {
if (isWindows) cancel("Test not supported on windows")
assert(out.contains[String]("-Dsbt.ivy.home=/ivy/dir"))
}
test("sbt --script-version should print sbtVersion") {
val out = sbtProcess("--script-version").!!.trim
val expectedVersion = "^"+SbtRunnerTest.versionRegEx+"$"
assert(out.matches(expectedVersion))
()
}
makeTest("--sbt-cache")("--sbt-cache", "./cachePath") { out: List[String] =>
assert(out.contains[String](s"-Dsbt.global.localcache=./cachePath"))
}
}

View File

@ -42,6 +42,8 @@ set sbt_args_ivy=
set sbt_args_supershell=
set sbt_args_timings=
set sbt_args_traces=
set sbt_args_sbt_boot=
set sbt_args_sbt_cache=
set sbt_args_sbt_create=
set sbt_args_sbt_dir=
set sbt_args_sbt_version=
@ -259,6 +261,21 @@ if defined _sbt_boot_arg (
)
)
if "%~0" == "-sbt-cache" set _sbt_cache_arg=true
if "%~0" == "--sbt-cache" set _sbt_cache_arg=true
if defined _sbt_cache_arg (
set _sbt_cache_arg=
if not "%~1" == "" (
set sbt_args_sbt_cache=%1
shift
goto args_loop
) else (
echo "%~0" is missing a value
goto error
)
)
if "%~0" == "-sbt-jar" set _sbt_jar=true
if "%~0" == "--sbt-jar" set _sbt_jar=true
@ -587,6 +604,10 @@ if defined sbt_args_sbt_boot (
set _SBT_OPTS=-Dsbt.boot.directory=!sbt_args_sbt_boot! !_SBT_OPTS!
)
if defined sbt_args_sbt_cache (
set _SBT_OPTS=-Dsbt.global.localcache=!sbt_args_sbt_cache! !_SBT_OPTS!
)
if defined sbt_args_ivy (
set _SBT_OPTS=-Dsbt.ivy.home=!sbt_args_ivy! !_SBT_OPTS!
)
@ -932,6 +953,7 @@ echo --timings display task timings report on shutdown
echo --sbt-create start sbt even if current directory contains no sbt project
echo --sbt-dir ^<path^> path to global settings/plugins directory ^(default: ~/.sbt^)
echo --sbt-boot ^<path^> path to shared boot directory ^(default: ~/.sbt/boot in 0.11 series^)
echo --sbt-cache ^<path^> path to global cache directory ^(default: operating system specific^)
echo --ivy ^<path^> path to local Ivy repository ^(default: ~/.ivy2^)
echo --mem ^<integer^> set memory options ^(default: %sbt_default_mem%^)
echo --no-share use all local caches; no sharing

View File

@ -71,7 +71,7 @@ final class Console(compiler: AnalyzingCompiler) {
terminal.withRawOutput {
jline.TerminalFactory.set(terminal.toJLine)
DeprecatedJLine.setTerminalOverride(jline3term)
terminal.withRawInput(Run.executeTrapExit(console0, log))
terminal.withRawInput(Run.executeSuccess(console0))
}
} finally {
sys.props("scala.color") = previous

View File

@ -973,7 +973,7 @@ object Defaults extends BuildCommon {
val old = scalacOptions.value
if (sbtPlugin.value && VersionNumber(scalaVersion.value)
.matchesSemVer(SemanticSelector("=2.12 >=2.12.13")))
old ++ Seq("-Wconf:cat=unused-nowarn:s")
old ++ Seq("-Wconf:cat=unused-nowarn:s", "-Xsource:3")
else old
},
persistJarClasspath :== true,

View File

@ -281,7 +281,7 @@ object Keys {
val runMain = inputKey[Unit]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask)
val discoveredMainClasses = taskKey[Seq[String]]("Auto-detects main classes.").withRank(BMinusTask)
val runner = taskKey[ScalaRun]("Implementation used to run a main class.").withRank(DTask)
val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This is currently only suitable for serially-executed 'run'-like tasks.").withRank(CSetting)
val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This was removed in sbt 1.6.0 due to JDK 17 deprecating Security Manager.").withRank(CSetting)
val fork = settingKey[Boolean]("If true, forks a new JVM when running. If false, runs in the same JVM as the build.").withRank(ASetting)
val forkOptions = taskKey[ForkOptions]("Configures JVM forking.").withRank(DSetting)
@ -392,7 +392,7 @@ object Keys {
val pushRemoteCacheTo = settingKey[Option[Resolver]]("The resolver to publish remote cache to.")
val remoteCacheResolvers = settingKey[Seq[Resolver]]("Resolvers for remote cache.")
val remoteCachePom = taskKey[File]("Generates a pom for publishing when publishing Maven-style.")
val localCacheDirectory = settingKey[File]("Directory to pull the remote cache to.")
val localCacheDirectory = settingKey[File]("Operating system specific cache directory.")
val usePipelining = settingKey[Boolean]("Use subproject pipelining for compilation.").withRank(BSetting)
val exportPipelining = settingKey[Boolean]("Product early output so downstream subprojects can do pipelining.").withRank(BSetting)

View File

@ -210,21 +210,16 @@ object StandardMain {
private[this] val isShutdown = new AtomicBoolean(false)
def runManaged(s: State): xsbti.MainResult = {
val previous = TrapExit.installManager()
val hook = ShutdownHooks.add(closeRunnable)
try {
val hook = ShutdownHooks.add(closeRunnable)
try {
MainLoop.runLogged(s)
} catch {
case _: InterruptedException if isShutdown.get =>
new xsbti.Exit { override def code(): Int = 0 }
} finally {
try DefaultBackgroundJobService.shutdown()
finally hook.close()
()
}
MainLoop.runLogged(s)
} catch {
case _: InterruptedException if isShutdown.get =>
new xsbti.Exit { override def code(): Int = 0 }
} finally {
TrapExit.uninstallManager(previous)
try DefaultBackgroundJobService.shutdown()
finally hook.close()
()
}
}

View File

@ -99,7 +99,7 @@ private[sbt] object PluginCross {
VersionNumber(sv) match {
case VersionNumber(Seq(0, 12, _*), _, _) => "2.9.2"
case VersionNumber(Seq(0, 13, _*), _, _) => "2.10.7"
case VersionNumber(Seq(1, 0, _*), _, _) => "2.12.14"
case VersionNumber(Seq(1, 0, _*), _, _) => "2.12.15"
case _ => sys.error(s"Unsupported sbt binary version: $sv")
}
}

View File

@ -26,7 +26,7 @@ object SemanticdbPlugin extends AutoPlugin {
semanticdbEnabled := SysProp.semanticdb,
semanticdbIncludeInJar := false,
semanticdbOptions := List(),
semanticdbVersion := "4.4.20"
semanticdbVersion := "4.4.28"
)
override lazy val projectSettings: Seq[Def.Setting[_]] = Seq(

View File

@ -4,7 +4,7 @@ import sbt.contraband.ContrabandPlugin.autoImport._
object Dependencies {
// WARNING: Please Scala update versions in PluginCross.scala too
val scala212 = "2.12.14"
val scala212 = "2.12.15"
val scala213 = "2.13.6"
val checkPluginCross = settingKey[Unit]("Make sure scalaVersion match up")
val baseScalaVersion = scala212
@ -12,10 +12,10 @@ object Dependencies {
sys.env.get("BUILD_VERSION") orElse sys.props.get("sbt.build.version")
// sbt modules
private val ioVersion = nightlyVersion.getOrElse("1.5.1")
private val ioVersion = nightlyVersion.getOrElse("1.6.0-M1")
private val lmVersion =
sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.5.2")
val zincVersion = nightlyVersion.getOrElse("1.5.5")
sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.6.0-M1")
val zincVersion = nightlyVersion.getOrElse("1.6.0-M1")
private val sbtIO = "org.scala-sbt" %% "io" % ioVersion
@ -116,5 +116,5 @@ object Dependencies {
val hedgehog = "qa.hedgehog" %% "hedgehog-sbt" % "0.6.1"
val disruptor = "com.lmax" % "disruptor" % "3.4.2"
val kindProjector = ("org.typelevel" % "kind-projector" % "0.13.0").cross(CrossVersion.full)
val kindProjector = ("org.typelevel" % "kind-projector" % "0.13.2").cross(CrossVersion.full)
}

View File

@ -58,6 +58,7 @@ class ForkRun(config: ForkOptions) extends ScalaRun {
private def classpathOption(classpath: Seq[File]) =
"-classpath" :: Path.makeString(classpath) :: Nil
}
class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolean)
extends ScalaRun {
def this(instance: ScalaInstance, trapExit: Boolean, nativeTmp: File) =
@ -105,9 +106,8 @@ class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolea
// log.trace(e)
throw e
}
// try { execute(); None } catch { case e: Exception => log.trace(e); Some(e.toString) }
if (trapExit) Run.executeTrapExit(execute(), log)
if (trapExit) Run.executeSuccess(execute())
else directExecute()
}
@ -169,12 +169,12 @@ object Run {
runner.run(mainClass, classpath, options, log)
/** Executes the given function, trapping calls to System.exit. */
def executeTrapExit(f: => Unit, log: Logger): Try[Unit] = {
val exitCode = TrapExit(f, log)
if (exitCode == 0) {
log.debug("Exited with code 0")
Success(())
} else Failure(new MessageOnlyException("Nonzero exit code: " + exitCode))
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def executeTrapExit(f: => Unit, log: Logger): Try[Unit] = executeSuccess(f)
private[sbt] def executeSuccess(f: => Unit): Try[Unit] = {
f
Success(())
}
// quotes the option that includes a whitespace

View File

@ -7,19 +7,7 @@
package sbt
import scala.reflect.Manifest
import scala.collection.concurrent.TrieMap
import java.lang.ref.WeakReference
import Thread.currentThread
import java.security.Permission
import java.util.concurrent.{ ConcurrentHashMap => CMap }
import java.lang.Integer.{ toHexString => hex }
import java.util.function.Supplier
import sbt.util.Logger
import sbt.util.InterfaceUtil
import sbt.internal.util.Util.{ AnyOps, none }
import TrapExit._
/**
* Provides an approximation to isolated execution within a single JVM.
@ -38,25 +26,21 @@ object TrapExit {
* Run `execute` in a managed context, using `log` for debugging messages.
* `installManager` must be called before calling this method.
*/
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def apply(execute: => Unit, log: Logger): Int =
System.getSecurityManager match {
case m: TrapExit => m.runManaged(InterfaceUtil.toSupplier(execute), log)
case _ => runUnmanaged(execute, log)
}
runUnmanaged(execute, log)
/**
* Installs the SecurityManager that implements the isolation and returns the previously installed SecurityManager, which may be null.
* This method must be called before using `apply`.
*/
def installManager(): SecurityManager =
System.getSecurityManager match {
case m: TrapExit => m
case m => System.setSecurityManager(new TrapExit(m)); m
}
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def installManager(): Nothing =
sys.error("TrapExit feature is removed due to JDK 17 deprecating SecurityManager")
/** Uninstalls the isolation SecurityManager and restores the old security manager. */
def uninstallManager(previous: SecurityManager): Unit =
System.setSecurityManager(previous)
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def uninstallManager(previous: Any): Unit = ()
private[this] def runUnmanaged(execute: => Unit, log: Logger): Int = {
log.warn("Managed execution not possible: security manager not installed.")
@ -69,473 +53,4 @@ object TrapExit {
1
}
}
private type ThreadID = String
/** `true` if the thread `t` is in the TERMINATED state.x*/
private def isDone(t: Thread): Boolean = t.getState == Thread.State.TERMINATED
private def computeID(g: ThreadGroup): ThreadID =
s"g:${hex(System.identityHashCode(g))}:${g.getName}"
/** Computes an identifier for a Thread that has a high probability of being unique within a single JVM execution. */
private def computeID(t: Thread): ThreadID =
// can't use t.getId because when getAccess first sees a Thread, it hasn't been initialized yet
// can't use t.getName because calling it on AWT thread in certain circumstances generates a segfault (#997):
// Apple AWT: +[ThreadUtilities getJNIEnvUncached] attempting to attach current thread after JNFObtainEnv() failed
s"${hex(System.identityHashCode(t))}"
/** Waits for the given `thread` to terminate. However, if the thread state is NEW, this method returns immediately. */
private def waitOnThread(thread: Thread, log: Logger): Unit = {
log.debug("Waiting for thread " + thread.getName + " to terminate.")
thread.join
log.debug("\tThread " + thread.getName + " exited.")
}
// interrupts the given thread, but first replaces the exception handler so that the InterruptedException is not printed
private def safeInterrupt(thread: Thread, log: Logger): Unit = {
log.debug("Interrupting thread " + thread.getName)
thread.setUncaughtExceptionHandler(new TrapInterrupt(thread.getUncaughtExceptionHandler))
thread.interrupt
log.debug("\tInterrupted " + thread.getName)
}
// an uncaught exception handler that swallows InterruptedExceptions and otherwise defers to originalHandler
private final class TrapInterrupt(originalHandler: Thread.UncaughtExceptionHandler)
extends Thread.UncaughtExceptionHandler {
def uncaughtException(thread: Thread, e: Throwable): Unit = {
withCause[InterruptedException, Unit](e) { interrupted =>
()
} { other =>
originalHandler.uncaughtException(thread, e)
}
thread.setUncaughtExceptionHandler(originalHandler)
}
}
/**
* Recurses into the causes of the given exception looking for a cause of type CauseType. If one is found, `withType` is called with that cause.
* If not, `notType` is called with the root cause.
*/
private def withCause[CauseType <: Throwable, T](
e: Throwable
)(withType: CauseType => T)(notType: Throwable => T)(implicit mf: Manifest[CauseType]): T = {
val clazz = mf.runtimeClass
if (clazz.isInstance(e))
withType(e.asInstanceOf[CauseType])
else {
val cause = e.getCause
if (cause == null)
notType(e)
else
withCause(cause)(withType)(notType)(mf)
}
}
}
/**
* Simulates isolation via a SecurityManager.
* Multiple applications are supported by tracking Thread constructions via `checkAccess`.
* The Thread that constructed each Thread is used to map a new Thread to an application.
* This is not reliable on all jvms, so ThreadGroup creations are also tracked via
* `checkAccess` and traversed on demand to collect threads.
* This association of Threads with an application allows properly waiting for
* non-daemon threads to terminate or to interrupt the correct threads when terminating.
* It also allows disposing AWT windows if the application created any.
* Only one AWT application is supported at a time, however.
*/
private final class TrapExit(delegateManager: SecurityManager) extends SecurityManager {
/** Tracks the number of running applications in order to short-cut SecurityManager checks when no applications are active.*/
private[this] val running = new java.util.concurrent.atomic.AtomicInteger
/** Maps a thread or thread group to its originating application. The thread is represented by a unique identifier to avoid leaks. */
private[this] val threadToApp = new CMap[ThreadID, App]
/** Executes `f` in a managed context. */
def runManaged(f: Supplier[Unit], xlog: xsbti.Logger): Int = {
val _ = running.incrementAndGet()
try runManaged0(f, xlog)
finally {
running.decrementAndGet(); ()
}
}
private[this] def runManaged0(f: Supplier[Unit], xlog: xsbti.Logger): Int = {
val log: Logger = xlog
val app = new App(f, log)
val executionThread = app.mainThread
try {
executionThread.start() // thread actually evaluating `f`
finish(app, log)
} catch {
case _: InterruptedException => // here, the thread that started the run has been interrupted, not the main thread of the executing code
cancel(executionThread, app, log)
} finally app.cleanup()
}
/** Interrupt all threads and indicate failure in the exit code. */
private[this] def cancel(executionThread: Thread, app: App, log: Logger): Int = {
log.warn("Run canceled.")
executionThread.interrupt()
stopAllThreads(app)
1
}
/**
* Wait for all non-daemon threads for `app` to exit, for an exception to be thrown in the main thread,
* or for `System.exit` to be called in a thread started by `app`.
*/
private[this] def finish(app: App, log: Logger): Int = {
log.debug("Waiting for threads to exit or System.exit to be called.")
waitForExit(app)
log.debug("Interrupting remaining threads (should be all daemons).")
stopAllThreads(app) // should only be daemon threads left now
log.debug("Sandboxed run complete..")
app.exitCode.value.getOrElse(0)
}
// wait for all non-daemon threads to terminate
private[this] def waitForExit(app: App): Unit = {
var daemonsOnly = true
app.processThreads { thread =>
// check isAlive because calling `join` on a thread that hasn't started returns immediately
// and App will only remove threads that have terminated, which will make this method loop continuously
// if a thread is created but not started
if (thread.isAlive && !thread.isDaemon) {
daemonsOnly = false
waitOnThread(thread, app.log)
}
}
// processThreads takes a snapshot of the threads at a given moment, so if there were only daemons, the application should shut down
if (!daemonsOnly)
waitForExit(app)
}
/** Gives managed applications a unique ID to use in the IDs of the main thread and thread group. */
private[this] val nextAppID = new java.util.concurrent.atomic.AtomicLong
private def nextID(): String = nextAppID.getAndIncrement.toHexString
/**
* Represents an isolated application as simulated by [[TrapExit]].
* `execute` is the application code to evaluate.
* `log` is used for debug logging.
*/
private final class App(val execute: Supplier[Unit], val log: Logger) extends Runnable {
/**
* Tracks threads and groups created by this application.
* To avoid leaks, keys are a unique identifier and values are held via WeakReference.
* A TrieMap supports the necessary concurrent updates and snapshots.
*/
private[this] val threads = new TrieMap[ThreadID, WeakReference[Thread]]
private[this] val groups = new TrieMap[ThreadID, WeakReference[ThreadGroup]]
/** Tracks whether AWT has ever been used in this jvm execution. */
@volatile
var awtUsed = false
/** The unique ID of the application. */
val id = nextID()
/** The ThreadGroup to use to try to track created threads. */
val mainGroup: ThreadGroup = new ThreadGroup("run-main-group-" + id) {
private[this] val handler = new LoggingExceptionHandler(log, None)
override def uncaughtException(t: Thread, e: Throwable): Unit =
handler.uncaughtException(t, e)
}
val mainThread = new Thread(mainGroup, this, "run-main-" + id)
/** Saves the ids of the creating thread and thread group to avoid tracking them as coming from this application. */
val creatorThreadID = computeID(currentThread)
val creatorGroup = currentThread.getThreadGroup
register(mainThread)
register(mainGroup)
val exitCode = new ExitCode
def run(): Unit = {
try execute.get()
catch {
case x: Throwable =>
exitCode.set(1) //exceptions in the main thread cause the exit code to be 1
throw x
}
}
/** Records a new group both in the global [[TrapExit]] manager and for this [[App]].*/
def register(g: ThreadGroup): Unit =
if (g != null && g != creatorGroup && !isSystemGroup(g)) {
val groupID = computeID(g)
val old = groups.putIfAbsent(groupID, new WeakReference(g))
if (old.isEmpty) { // wasn't registered
threadToApp.put(groupID, this)
()
}
}
/**
* Records a new thread both in the global [[TrapExit]] manager and for this [[App]].
* Its uncaught exception handler is configured to log exceptions through `log`.
*/
def register(t: Thread): Unit = {
val threadID = computeID(t)
if (!isDone(t) && threadID != creatorThreadID) {
val old = threads.putIfAbsent(threadID, new WeakReference(t))
if (old.isEmpty) { // wasn't registered
threadToApp.put(threadID, this)
setExceptionHandler(t)
if (!awtUsed && isEventQueue(t))
awtUsed = true
}
}
}
/** Registers the logging exception handler on `t`, delegating to the existing handler if it isn't the default. */
private[this] def setExceptionHandler(t: Thread): Unit = {
val group = t.getThreadGroup
val previousHandler = t.getUncaughtExceptionHandler match {
case null | `group` | (_: LoggingExceptionHandler) => none[Thread.UncaughtExceptionHandler]
case x => x.some // delegate to a custom handler only
}
t.setUncaughtExceptionHandler(new LoggingExceptionHandler(log, previousHandler))
}
/** Removes a thread or group from this [[App]] and the global [[TrapExit]] manager. */
private[this] def unregister(id: ThreadID): Unit = {
threadToApp.remove(id)
threads.remove(id)
groups.remove(id)
()
}
/** Final cleanup for this application after it has terminated. */
def cleanup(): Unit = {
cleanup(threads)
cleanup(groups)
}
private[this] def cleanup(resources: TrieMap[ThreadID, _]): Unit = {
val snap = resources.readOnlySnapshot
resources.clear()
for ((id, _) <- snap)
unregister(id)
}
// only want to operate on unterminated threads
// want to drop terminated threads, including those that have been gc'd
/** Evaluates `f` on each `Thread` started by this [[App]] at single instant shortly after this method is called. */
def processThreads(f: Thread => Unit): Unit = {
// pulls in threads that weren't recorded by checkAccess(Thread) (which is jvm-dependent)
// but can be reached via the Threads in the ThreadGroups recorded by checkAccess(ThreadGroup) (not jvm-dependent)
addUntrackedThreads()
val snap = threads.readOnlySnapshot
for ((id, tref) <- snap) {
val t = tref.get
if ((t eq null) || isDone(t))
unregister(id)
else {
f(t)
if (isDone(t))
unregister(id)
}
}
}
// registers Threads from the tracked ThreadGroups
private[this] def addUntrackedThreads(): Unit =
groupThreadsSnapshot foreach register
private[this] def groupThreadsSnapshot: Seq[Thread] = {
val snap = groups.readOnlySnapshot.values.map(_.get).filter(_ != null)
threadsInGroups(snap.toList, Nil)
}
// takes a snapshot of the threads in `toProcess`, acquiring nested locks on each group to do so
// the thread groups are accumulated in `accum` and then the threads in each are collected all at
// once while they are all locked. This is the closest thing to a snapshot that can be accomplished.
private[this] def threadsInGroups(
toProcess: List[ThreadGroup],
accum: List[ThreadGroup]
): List[Thread] = toProcess match {
case group :: tail =>
// ThreadGroup implementation synchronizes on its methods, so by synchronizing here, we can workaround its quirks somewhat
group.synchronized {
// not tail recursive because of synchronized
threadsInGroups(threadGroups(group) ::: tail, group :: accum)
}
case Nil => accum.flatMap(threads)
}
// gets the immediate child ThreadGroups of `group`
private[this] def threadGroups(group: ThreadGroup): List[ThreadGroup] = {
val upperBound = group.activeGroupCount
val groups = new Array[ThreadGroup](upperBound)
val childrenCount = group.enumerate(groups, false)
groups.take(childrenCount).toList
}
// gets the immediate child Threads of `group`
private[this] def threads(group: ThreadGroup): List[Thread] = {
val upperBound = group.activeCount
val threads = new Array[Thread](upperBound)
val childrenCount = group.enumerate(threads, false)
threads.take(childrenCount).toList
}
}
private[this] def stopAllThreads(app: App): Unit = {
// only try to dispose frames if we think the App used AWT
// otherwise, we initialize AWT as a side effect of asking for the frames
// also, we only assume one AWT application at a time
if (app.awtUsed)
disposeAllFrames(app.log)
interruptAllThreads(app)
}
private[this] def interruptAllThreads(app: App): Unit =
app processThreads { t =>
if (!isSystemThread(t)) safeInterrupt(t, app.log)
else app.log.debug(s"Not interrupting system thread $t")
}
/** Gets the managed application associated with Thread `t` */
private[this] def getApp(t: Thread): Option[App] =
Option(threadToApp.get(computeID(t))) orElse getApp(t.getThreadGroup)
/** Gets the managed application associated with ThreadGroup `group` */
private[this] def getApp(group: ThreadGroup): Option[App] =
Option(group).flatMap(g => Option(threadToApp.get(computeID(g))))
/**
* Handles a valid call to `System.exit` by setting the exit code and
* interrupting remaining threads for the application associated with `t`, if one exists.
*/
private[this] def exitApp(t: Thread, status: Int): Unit = getApp(t) match {
case None => System.err.println(s"Could not exit($status): no application associated with $t")
case Some(a) =>
a.exitCode.set(status)
stopAllThreads(a)
}
/** SecurityManager hook to trap calls to `System.exit` to avoid shutting down the whole JVM.*/
override def checkExit(status: Int): Unit = if (active) {
val t = currentThread
val stack = t.getStackTrace
if (stack == null || stack.exists(isRealExit)) {
exitApp(t, status)
throw new TrapExitSecurityException(status)
}
}
/** This ensures that only actual calls to exit are trapped and not just calls to check if exit is allowed.*/
private def isRealExit(element: StackTraceElement): Boolean =
element.getClassName == "java.lang.Runtime" && element.getMethodName == "exit"
// These are overridden to do nothing because there is a substantial filesystem performance penalty
// when there is a SecurityManager defined. The default implementations of these construct a
// FilePermission, and its initialization involves canonicalization, which is expensive.
override def checkRead(file: String): Unit = ()
override def checkRead(file: String, context: AnyRef): Unit = ()
override def checkWrite(file: String): Unit = ()
override def checkDelete(file: String): Unit = ()
override def checkExec(cmd: String): Unit = ()
override def checkPermission(perm: Permission): Unit = {
if (delegateManager ne null)
delegateManager.checkPermission(perm)
}
override def checkPermission(perm: Permission, context: AnyRef): Unit = {
if (delegateManager ne null)
delegateManager.checkPermission(perm, context)
}
/**
* SecurityManager hook that is abused to record every created Thread and associate it with a managed application.
* This is not reliably called on different jvm implementations. On openjdk and similar jvms, the Thread constructor
* calls setPriority, which triggers this SecurityManager check. For Java 6 on OSX, this is not called, however.
*/
override def checkAccess(t: Thread): Unit = {
if (active) {
val group = t.getThreadGroup
noteAccess(group) { app =>
app.register(group)
app.register(t)
app.register(currentThread)
}
}
if (delegateManager ne null)
delegateManager.checkAccess(t)
}
/**
* This is specified to be called in every Thread's constructor and every time a ThreadGroup is created.
* This allows us to reliably track every ThreadGroup that is created and map it back to the constructing application.
*/
override def checkAccess(tg: ThreadGroup): Unit = {
if (active && !isSystemGroup(tg)) {
noteAccess(tg) { app =>
app.register(tg)
app.register(currentThread)
}
}
if (delegateManager ne null)
delegateManager.checkAccess(tg)
}
private[this] def noteAccess(group: ThreadGroup)(f: App => Unit): Unit =
getApp(currentThread) orElse getApp(group) foreach f
private[this] def isSystemGroup(group: ThreadGroup): Boolean =
(group != null) && (group.getName == "system")
/** `true` if there is at least one application currently being managed. */
private[this] def active = running.get > 0
private def disposeAllFrames(log: Logger): Unit = {
val allFrames = java.awt.Frame.getFrames
if (allFrames.nonEmpty) {
log.debug(s"Disposing ${allFrames.length} top-level windows...")
allFrames.foreach(_.dispose) // dispose all top-level windows, which will cause the AWT-EventQueue-* threads to exit
val waitSeconds = 2
log.debug(s"Waiting $waitSeconds s to let AWT thread exit.")
Thread.sleep(waitSeconds * 1000L) // AWT Thread doesn't exit immediately, so wait to interrupt it
}
}
/** Returns true if the given thread is in the 'system' thread group or is an AWT thread other than AWT-EventQueue.*/
private def isSystemThread(t: Thread) =
if (t.getName.startsWith("AWT-"))
!isEventQueue(t)
else
isSystemGroup(t.getThreadGroup)
/**
* An App is identified as using AWT if it gets associated with the event queue thread.
* The event queue thread is not treated as a system thread.
*/
private[this] def isEventQueue(t: Thread): Boolean = t.getName.startsWith("AWT-EventQueue")
}
/** A thread-safe, write-once, optional cell for tracking an application's exit code.*/
private final class ExitCode {
private var code: Option[Int] = None
def set(c: Int): Unit = synchronized { code = code orElse Some(c) }
def value: Option[Int] = synchronized { code }
}
/**
* The default uncaught exception handler for managed executions.
* It logs the thread and the exception.
*/
private final class LoggingExceptionHandler(
log: Logger,
delegate: Option[Thread.UncaughtExceptionHandler]
) extends Thread.UncaughtExceptionHandler {
def uncaughtException(t: Thread, e: Throwable): Unit = {
log.error("(" + t.getName + ") " + e.toString)
log.trace(e)
delegate.foreach(_.uncaughtException(t, e))
}
}

5
sbt
View File

@ -553,6 +553,7 @@ Usage: `basename "$0"` [options]
--sbt-create start sbt even if current directory contains no sbt project
--sbt-dir <path> path to global settings/plugins directory (default: ~/.sbt)
--sbt-boot <path> path to shared boot directory (default: ~/.sbt/boot in 0.11 series)
--sbt-cache <path> path to global cache directory (default: operating system specific)
--ivy <path> path to local Ivy repository (default: ~/.ivy2)
--mem <integer> set memory options (default: $sbt_default_mem)
--no-share use all local caches; no sharing
@ -663,6 +664,10 @@ process_args () {
-batch|--batch) exec </dev/null && shift ;;
-sbt-jar|--sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;;
-sbt-cache|--sbt-cache) require_arg path "$1" "$2" &&
sbt_cache="$2" &&
addJava "-Dsbt.global.localcache=$2" &&
shift 2 ;;
-sbt-version|--sbt-version) require_arg version "$1" "$2" && sbt_version="$2" && shift 2 ;;
-java-home|--java-home) require_arg path "$1" "$2" &&
java_cmd="$2/bin/java" &&

View File

@ -1,5 +1,6 @@
lazy val check = taskKey[Unit]("")
lazy val compile2 = taskKey[Unit]("")
lazy val scala212 = "2.12.15"
lazy val root = (project in file("."))
.aggregate(foo, bar, client)
@ -10,19 +11,19 @@ lazy val root = (project in file("."))
lazy val foo = project
.settings(
crossScalaVersions := Seq("2.12.14", "2.13.1"),
crossScalaVersions := Seq(scala212, "2.13.1"),
libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.0",
check := {
// This tests that +check will respect bar's crossScalaVersions and not switch
val x = (LocalProject("bar") / scalaVersion).value
assert(x == "2.12.14", s"$x == 2.12.12")
assert(x == scala212, s"$x == $scala212")
(Compile / compile).value
},
(Test / testOnly) := {
// This tests that +testOnly will respect bar's crossScalaVersions and not switch
val x = (LocalProject("bar") / scalaVersion).value
assert(x == "2.12.14", s"$x == 2.12.12")
assert(x == scala212, s"$x == $scala212")
val _ = (Test / testOnly).evaluated
},
compile2 := {
@ -35,7 +36,7 @@ lazy val foo = project
lazy val bar = project
.settings(
crossScalaVersions := Seq("2.12.14"),
crossScalaVersions := Seq(scala212),
check := (Compile / compile).value,
compile2 := (Compile / compile).value,
)
@ -46,14 +47,14 @@ lazy val baz = project
check := {
// This tests that +baz/check will respect bar's crossScalaVersions and not switch
val x = (LocalProject("bar") / scalaVersion).value
assert(x == "2.12.14", s"$x == 2.12.14")
assert(x == scala212, s"$x == $scala212")
(Compile / compile).value
},
)
lazy val client = project
.settings(
crossScalaVersions := Seq("2.12.14", "2.13.1"),
crossScalaVersions := Seq(scala212, "2.13.1"),
check := (Compile / compile).value,
compile2 := (Compile / compile).value,
)

View File

@ -17,7 +17,7 @@
## test + with command or alias
> clean
## for command cross building you do need crossScalaVerions on root
> set root/crossScalaVersions := Seq("2.12.14", "2.13.1")
> set root/crossScalaVersions := Seq("2.12.15", "2.13.1")
> + build
$ exists foo/target/scala-2.12
$ exists foo/target/scala-2.13

View File

@ -1,4 +1,4 @@
scalaVersion in ThisBuild := "2.12.3"
ThisBuild / scalaVersion := "2.12.15"
libraryDependencies ++= Seq(
"com.novocode" % "junit-interface" % "0.5" % Test,

View File

@ -1,4 +1,4 @@
scalaVersion := "2.9.2"
ThisBuild / scalaVersion := "2.12.15"
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % "1.7.2",

View File

@ -1,5 +1,5 @@
// ThisBuild / useCoursier := false
ThisBuild / scalaVersion := "2.12.6"
ThisBuild / scalaVersion := "2.12.15"
ThisBuild / organization := "org.example"
ThisBuild / version := "0.1"

View File

@ -1,6 +1,6 @@
lazy val root = project.in(file("."))
.enablePlugins(SbtPlugin)
.settings(
scalaVersion := "2.12.14",
scalaVersion := "2.12.15",
scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint")
)

View File

@ -1,6 +1,6 @@
lazy val root = project.in(file("."))
.settings(
scalaVersion := "2.12.14",
scalaVersion := "2.12.15",
sbtPlugin := true,
scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint")
)

View File

@ -1,3 +1,4 @@
// Don't have to upgrade this while updating 2.12
ThisBuild / scalaVersion := "2.12.14"
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := "4.4.20"

View File

@ -1,4 +1,4 @@
ThisBuild / scalaVersion := "2.12.12"
ThisBuild / scalaVersion := "2.12.15"
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbIncludeInJar := true

View File

@ -1,3 +1,5 @@
ThisBuild / scalaVersion := "2.12.15"
import sbt.internal.CommandStrings.{ inspectBrief, inspectDetailed }
import sbt.internal.Inspect
import sjsonnew._, BasicJsonProtocol._
@ -11,8 +13,6 @@ val buildInfo = taskKey[Seq[File]]("The task that generates the build info.")
lazy val root = (project in file("."))
.settings(
Global / cancelable := true,
ThisBuild / scalaVersion := "2.12.3",
console / scalacOptions += "-deprecation",
Compile / console / scalacOptions += "-Ywarn-numeric-widen",
projA / Compile / console / scalacOptions += "-feature",

View File

@ -1,12 +0,0 @@
# Marked as pending, see https://github.com/sbt/sbt/issues/1543
# Tests if source dependencies are tracked properly
# for compile-time constants (like final vals in top-level objects)
# see https://issues.scala-lang.org/browse/SI-7173 for details
# why compile-time constants can be tricky to track due to early inlining
$ copy-file changes/B.scala B.scala
$ copy-file changes/A1.scala A.scala
> run 1
$ copy-file changes/A2.scala A.scala
> run 2

View File

@ -0,0 +1,16 @@
> ++2.12.15!
$ copy-file changes/B.scala B.scala
$ copy-file changes/A1.scala A.scala
> run 1
$ copy-file changes/A2.scala A.scala
> run 2
> clean
> ++2.13.6!
$ copy-file changes/A1.scala A.scala
> run 1
$ copy-file changes/A2.scala A.scala
> run 2

View File

@ -3,7 +3,7 @@ import sbt.internal.inc.ScalaInstance
lazy val OtherScala = config("other-scala").hide
lazy val junitinterface = "com.novocode" % "junit-interface" % "0.11"
lazy val akkaActor = "com.typesafe.akka" %% "akka-actor" % "2.5.17"
ThisBuild / scalaVersion := "2.12.14"
ThisBuild / scalaVersion := "2.12.15"
lazy val root = (project in file("."))
.configs(OtherScala)

View File

@ -1,6 +1,6 @@
import sbt.internal.server.{ ServerHandler, ServerIntent }
ThisBuild / scalaVersion := "2.12.14"
ThisBuild / scalaVersion := "2.12.15"
Global / serverLog / logLevel := Level.Debug
// custom handler

View File

@ -102,22 +102,23 @@ object BuildServerTest extends AbstractServerTest {
|} }""".stripMargin
)
assert(svr.waitForString(10.seconds) { s =>
// This doesn't always come back in 10s on CI.
assert(svr.waitForString(60.seconds) { s =>
s.contains("build/taskStart") &&
s.contains(""""message":"Compiling runAndTest"""")
})
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(60.seconds) { s =>
s.contains("build/taskProgress") &&
s.contains(""""message":"Compiling runAndTest (15%)"""")
})
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(60.seconds) { s =>
s.contains("build/taskProgress") &&
s.contains(""""message":"Compiling runAndTest (100%)"""")
})
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(60.seconds) { s =>
s.contains("build/taskFinish") &&
s.contains(""""message":"Compiled runAndTest"""")
})

View File

@ -13,7 +13,7 @@ import java.util.concurrent.atomic.AtomicInteger
// starts svr using server-test/events and perform event related tests
object EventsTest extends AbstractServerTest {
override val testDirectory: String = "events"
val currentID = new AtomicInteger(0)
val currentID = new AtomicInteger(1000)
test("report task failures in case of exceptions") { _ =>
val id = currentID.getAndIncrement()
@ -30,9 +30,7 @@ object EventsTest extends AbstractServerTest {
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":$id, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
s contains "Waiting for"
})
Thread.sleep(1000)
val cancelID = currentID.getAndIncrement()
val invalidID = currentID.getAndIncrement()
svr.sendJsonRpc(
@ -41,52 +39,49 @@ object EventsTest extends AbstractServerTest {
assert(svr.waitForString(20.seconds) { s =>
(s contains """"error":{"code":-32800""")
})
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":${currentID.getAndIncrement}, "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
s contains """"result":{"status":"Task cancelled""""
})
}
/*
test("cancel on-going task with numeric id") { _ =>
val id = currentID.getAndIncrement()
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":$id, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(20.seconds) { s =>
s contains "Compiled events"
})
assert(svr.waitForString(10.seconds) { s =>
s contains "Waiting for"
s contains "running Main"
})
val cancelID = currentID.getAndIncrement()
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":$cancelID, "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(11.seconds) { s =>
println(s)
s contains """"result":{"status":"Task cancelled""""
})
}
*/
/*
test("cancel on-going task with string id") { _ =>
import sbt.Exec
val id = Exec.newExecId
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "$id", "method": "sbt/exec", "params": { "commandLine": "run" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(20.seconds) { s =>
s contains "Compiled events"
})
assert(svr.waitForString(10.seconds) { s =>
s contains "Waiting for"
})
val cancelID = Exec.newExecId
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "$cancelID", "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(11.seconds) { s =>
println(s)
s contains """"result":{"status":"Task cancelled""""
})
}
*/
}