mirror of
https://github.com/sbt/sbt.git
synced 2026-08-30 17:54:25 +02:00
Failure caching assumes a CompileFailed is a function of
the sources, which is true for source errors. But zinc also surfaces I/O write
failures ("error writing X.class") as compiler problems, so an environmental
failure (a concurrent target/ deletion, a permission blip) was cached under the
same mechanism and replayed from the global action cache on every later build,
even after the cause was gone. When the poisoned task is the metabuild compile
this is self-sustaining and unrecoverable from inside sbt: project loading
fails, so no task -- including clean -- can run, and only deleting the global
cache by hand recovers. The replayed diagnostics also name files/permissions
that no longer exist.
Refs #9455
Co-authored-by: BrianHotopp <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
197 lines
7.1 KiB
Scala
197 lines
7.1 KiB
Scala
package local
|
|
|
|
import java.lang.reflect.InvocationTargetException
|
|
|
|
import sbt.*
|
|
import sbt.internal.inc.ScalaInstance
|
|
import sbt.internal.inc.classpath.{ ClasspathUtilities, FilteredLoader }
|
|
import scala.annotation.nowarn
|
|
import scala.collection.JavaConverters.*
|
|
|
|
object LocalScriptedPlugin extends AutoPlugin {
|
|
override def requires = plugins.JvmPlugin
|
|
|
|
object autoImport extends ScriptedKeys
|
|
}
|
|
|
|
trait ScriptedKeys {
|
|
val publishLocalBinAll = taskKey[Unit]("")
|
|
val scriptedUnpublished = inputKey[Unit](
|
|
"Execute scripted without publishing sbt first. " +
|
|
"Saves you some time when only your test has changed"
|
|
)
|
|
val scriptedSource = settingKey[File]("")
|
|
val scriptedPrescripted = taskKey[File => Unit]("")
|
|
val scriptedKeepTempDirectory = settingKey[Boolean](
|
|
"If true, keeps the temporary directory after scripted tests complete for debugging."
|
|
)
|
|
}
|
|
|
|
object Scripted {
|
|
// This is to workaround https://github.com/sbt/io/issues/110
|
|
if (!sys.props.contains("jna.nosys")) sys.props.put("jna.nosys", "true")
|
|
|
|
val RepoOverrideTest = config("repoOverrideTest") extend Compile
|
|
|
|
val sbtWindowsExcludeFilter: FileFilter =
|
|
if (scala.util.Properties.isWin)
|
|
new SimpleFileFilter(f =>
|
|
(f.getParentFile.getName, f.getName) match {
|
|
case ("classloader-cache", "jni") => true // no native lib is built for windows
|
|
case ("classloader-cache", "spark") =>
|
|
true // the test spark server is unable to bind to a local socket on Visual Studio 2019
|
|
case ("nio", "make-clone") => true // uses gcc which isn't set up on all systems
|
|
case ("watch", "symlinks") => true // symlinks don't work the same on windows
|
|
case ("cache", "compile-io-failure") =>
|
|
true // a read-only dir doesn't block writes on windows, so the I/O failure won't reproduce
|
|
case _ => false
|
|
}
|
|
)
|
|
else NothingFilter
|
|
|
|
import sbt.complete.*
|
|
|
|
// Paging, 1-index based.
|
|
final case class ScriptedTestPage(page: Int, total: Int)
|
|
|
|
// FIXME: Duplicated with ScriptedPlugin.scriptedParser, this can be
|
|
// avoided once we upgrade build.properties to 0.13.14
|
|
def scriptedParser(scriptedBase: File): Parser[Seq[String]] = {
|
|
import DefaultParsers.*
|
|
|
|
val scriptedFiles: NameFilter = ("test": NameFilter) | "pending"
|
|
val pairs = (scriptedBase * AllPassFilter * AllPassFilter * scriptedFiles).get() map {
|
|
(f: File) =>
|
|
val p = f.getParentFile
|
|
(p.getParentFile.getName, p.getName)
|
|
}
|
|
val pairMap = pairs.groupBy(_._1).mapValues(_.map(_._2).toSet)
|
|
|
|
val id = charClass(c => !c.isWhitespace && c != '/').+.string
|
|
val groupP = token(id.examples(pairMap.keySet)) <~ token('/')
|
|
|
|
// A parser for page definitions
|
|
val pageNumber = (NatBasic & not('0', "zero page number")).flatMap { i =>
|
|
if (i <= pairs.size) Parser.success(i)
|
|
else Parser.failure(s"$i exceeds the number of tests (${pairs.size})")
|
|
}
|
|
val pageP: Parser[ScriptedTestPage] = ("*" ~> pageNumber ~ ("of" ~> pageNumber)) flatMap {
|
|
case (page, total) if page <= total => success(ScriptedTestPage(page, total))
|
|
case (page, total) => failure(s"Page $page was greater than $total")
|
|
}
|
|
|
|
// Grabs the filenames from a given test group in the current page definition.
|
|
def pagedFilenames(group: String, page: ScriptedTestPage): Seq[String] = {
|
|
val files = pairMap.get(group).toSeq.flatten.sortBy(_.toLowerCase)
|
|
val pageSize = if (page.total == 0) files.size else files.size / page.total
|
|
// The last page may loose some values, so we explicitly keep them
|
|
val dropped = files.drop(pageSize * (page.page - 1))
|
|
if (page.page == page.total) dropped
|
|
else dropped.take(pageSize)
|
|
}
|
|
|
|
def nameP(group: String) = {
|
|
token("*".id | id.examples(pairMap.getOrElse(group, Set.empty[String])))
|
|
}
|
|
|
|
val PagedIds: Parser[Seq[String]] =
|
|
for {
|
|
group <- groupP
|
|
page <- pageP
|
|
files = pagedFilenames(group, page)
|
|
// TODO - Fail the parser if we don't have enough files for the given page size
|
|
// if !files.isEmpty
|
|
} yield files map (f => s"$group/$f")
|
|
|
|
val testID = (for (group <- groupP; name <- nameP(group)) yield (group, name))
|
|
val testIdAsGroup = matched(testID) map (test => Seq(test))
|
|
|
|
// (token(Space) ~> matched(testID)).*
|
|
(token(Space) ~> (PagedIds | testIdAsGroup)).* map (_.flatten)
|
|
}
|
|
|
|
@nowarn
|
|
def doScripted(
|
|
scriptedSbtInstance: ScalaInstance,
|
|
sourcePath: File,
|
|
bufferLog: Boolean,
|
|
args: Seq[String],
|
|
prescripted: File => Unit,
|
|
launchOpts: Seq[String],
|
|
scalaVersion: String,
|
|
sbtVersion: String,
|
|
classpath: Seq[File],
|
|
launcherJar: File,
|
|
logger: Logger,
|
|
keepTempDirectory: Boolean,
|
|
includeFilter: java.io.FileFilter,
|
|
excludeFilter: java.io.FileFilter,
|
|
): Unit = {
|
|
logger.info(s"Tests selected: ${args.mkString("\n * ", "\n * ", "\n")}")
|
|
logger.info("")
|
|
|
|
// Force Log4J to not use a thread context classloader otherwise it throws a CCE
|
|
sys.props(org.apache.logging.log4j.util.LoaderUtil.IGNORE_TCCL_PROPERTY) = "true"
|
|
|
|
val noJLine = new FilteredLoader(scriptedSbtInstance.loader, "jline." :: Nil)
|
|
val loader = ClasspathUtilities.toLoader(classpath, noJLine)
|
|
val bridgeClass = Class.forName("sbt.scriptedtest.ScriptedRunner", true, loader)
|
|
|
|
// Interface to cross class loader
|
|
type SbtScriptedRunner = {
|
|
def runInParallel(
|
|
resourceBaseDirectory: File,
|
|
bufferLog: Boolean,
|
|
tests: java.util.List[String],
|
|
launcherJar: File,
|
|
javaCommand: String,
|
|
launchOpts: java.util.List[String],
|
|
prescripted: java.util.List[File],
|
|
instance: Int,
|
|
keepTempDirectory: Boolean,
|
|
includeFilter: java.io.FileFilter,
|
|
excludeFilter: java.io.FileFilter,
|
|
): Unit
|
|
}
|
|
|
|
val initLoader = Thread.currentThread.getContextClassLoader
|
|
try {
|
|
Thread.currentThread.setContextClassLoader(loader)
|
|
val bridge =
|
|
bridgeClass.getDeclaredConstructor().newInstance().asInstanceOf[SbtScriptedRunner]
|
|
try {
|
|
// Using java.util.List to encode File => Unit.
|
|
val callback = new java.util.AbstractList[File] {
|
|
override def add(x: File): Boolean = { prescripted(x); false }
|
|
def get(x: Int): sbt.File = ???
|
|
def size(): Int = 0
|
|
}
|
|
val instances: Int = (System.getProperty("sbt.scripted.parallel.instances") match {
|
|
case null => 1
|
|
case i => scala.util.Try(i.toInt).getOrElse(1)
|
|
}) match {
|
|
case i if i > 0 => i
|
|
case _ => 1
|
|
}
|
|
import scala.language.reflectiveCalls
|
|
|
|
bridge.runInParallel(
|
|
sourcePath,
|
|
bufferLog,
|
|
args.toList.asJava,
|
|
launcherJar,
|
|
"java",
|
|
launchOpts.toList.asJava,
|
|
callback,
|
|
instances,
|
|
keepTempDirectory,
|
|
includeFilter,
|
|
excludeFilter,
|
|
)
|
|
} catch { case ite: InvocationTargetException => throw ite.getCause }
|
|
} finally {
|
|
Thread.currentThread.setContextClassLoader(initLoader)
|
|
}
|
|
}
|
|
}
|