mirror of https://github.com/sbt/sbt.git
[2.x] fix: Reject java.io.File as cached task output type (#8766)
Change the behavior when a cached task's output type contains java.io.File: instead of silently skipping the cache, throw a compile-time error with a message recommending xsbti.VirtualFileRef, xsbti.HashedVirtualFileRef, or xsbti.VirtualFile, and linking to the caching documentation. Internal sbt tasks that return File types are wrapped with Def.uncached to opt out of caching. Fixes https://github.com/sbt/sbt/issues/8762 Generated-by: GitHub Copilot (Claude) * [2.x] fix: Reject java.io.File as cached task output type Use @transient on File-returning keys in Keys.scala instead of wrapping tasks with Def.uncached in Defaults.scala. This ensures build users who rewire these tasks also get caching skipped automatically. - Add @transient to 19 File-returning taskKey definitions in Keys.scala - Revert Def.uncached wrappers from Defaults.scala - Error at compile time when File is used as cached task output type - Update error message to recommend @transient and link to docs - Update scripted tests to use @transient approach Fixes #8762 Generated-by: GitHub Copilot (Claude Opus 4.6)
This commit is contained in:
parent
4a5701cb8e
commit
a7d5f45515
|
|
@ -178,6 +178,16 @@ trait Cont:
|
|||
case Left(l) => (l, TypeRepr.of[Effect[A]])
|
||||
case Right(r) => (r, faTpe)
|
||||
|
||||
def containsFileType[A1: Type]: Boolean =
|
||||
val fileRepr = TypeRepr.of[java.io.File]
|
||||
def containsFile(tpe: TypeRepr): Boolean =
|
||||
if tpe =:= fileRepr then true
|
||||
else
|
||||
tpe.dealias match
|
||||
case AppliedType(_, args) => args.exists(containsFile)
|
||||
case _ => false
|
||||
containsFile(TypeRepr.of[A1])
|
||||
|
||||
val inputBuf = ListBuffer[Input]()
|
||||
val outputBuf = ListBuffer[Output]()
|
||||
|
||||
|
|
@ -342,38 +352,49 @@ trait Cont:
|
|||
cacheConfigExpr: Expr[BuildWideCacheConfiguration],
|
||||
tags: List[CacheLevelTag],
|
||||
)(body: Expr[A1], input: Expr[A2]): Expr[A1] =
|
||||
val codeContentHash =
|
||||
try Expr[Long](body.show.##)
|
||||
catch
|
||||
case e: Throwable =>
|
||||
Expr[Long](Printer.TreeStructure.show(body.asTerm).##)
|
||||
val extraHash = Expr[Long](0L)
|
||||
val aJsonFormat = summonJsonFormat[A1]
|
||||
val aClassTag = summonClassTag[A1]
|
||||
val inputHashWriter =
|
||||
if TypeRepr.of[A2] =:= TypeRepr.of[Unit] then
|
||||
'{
|
||||
import BasicJsonProtocol.*
|
||||
summon[HashWriter[Unit]]
|
||||
}.asExprOf[HashWriter[A2]]
|
||||
else summonHashWriter[A2]
|
||||
val tagsExpr = '{ List(${ Varargs(tags.map(Expr[CacheLevelTag](_))) }*) }
|
||||
val block = letOutput(outputs, cacheConfigExpr)(body)
|
||||
'{
|
||||
given HashWriter[A2] = $inputHashWriter
|
||||
given JsonFormat[A1] = $aJsonFormat
|
||||
given ClassTag[A1] = $aClassTag
|
||||
ActionCache
|
||||
.cache(
|
||||
$input,
|
||||
codeContentHash = Digest.dummy($codeContentHash),
|
||||
extraHash = Digest.dummy($extraHash),
|
||||
tags = $tagsExpr,
|
||||
config = $cacheConfigExpr,
|
||||
)({ _ =>
|
||||
$block
|
||||
})
|
||||
}
|
||||
if containsFileType[A1] then
|
||||
report.errorAndAbort(
|
||||
s"""java.io.File is not a valid output type for a cached task.
|
||||
|Consider using one of the following alternatives:
|
||||
| - xsbti.HashedVirtualFileRef
|
||||
| - xsbti.VirtualFileRef
|
||||
| - xsbti.VirtualFile
|
||||
|If caching is not needed, annotate the key with @transient, or wrap the task in Def.uncached.
|
||||
|See https://www.scala-sbt.org/2.x/docs/en/concepts/caching.html#caching-files""".stripMargin
|
||||
)
|
||||
else
|
||||
val codeContentHash =
|
||||
try Expr[Long](body.show.##)
|
||||
catch
|
||||
case e: Throwable =>
|
||||
Expr[Long](Printer.TreeStructure.show(body.asTerm).##)
|
||||
val extraHash = Expr[Long](0L)
|
||||
val aJsonFormat = summonJsonFormat[A1]
|
||||
val aClassTag = summonClassTag[A1]
|
||||
val inputHashWriter =
|
||||
if TypeRepr.of[A2] =:= TypeRepr.of[Unit] then
|
||||
'{
|
||||
import BasicJsonProtocol.*
|
||||
summon[HashWriter[Unit]]
|
||||
}.asExprOf[HashWriter[A2]]
|
||||
else summonHashWriter[A2]
|
||||
val tagsExpr = '{ List(${ Varargs(tags.map(Expr[CacheLevelTag](_))) }*) }
|
||||
val block = letOutput(outputs, cacheConfigExpr)(body)
|
||||
'{
|
||||
given HashWriter[A2] = $inputHashWriter
|
||||
given JsonFormat[A1] = $aJsonFormat
|
||||
given ClassTag[A1] = $aClassTag
|
||||
ActionCache
|
||||
.cache(
|
||||
$input,
|
||||
codeContentHash = Digest.dummy($codeContentHash),
|
||||
extraHash = Digest.dummy($extraHash),
|
||||
tags = $tagsExpr,
|
||||
config = $cacheConfigExpr,
|
||||
)({ _ =>
|
||||
$block
|
||||
})
|
||||
}
|
||||
|
||||
// This will generate following code for Def.declareOutput(...):
|
||||
// var $o1: VirtualFile = null
|
||||
|
|
|
|||
|
|
@ -157,9 +157,12 @@ object Keys {
|
|||
val javaSource = settingKey[File]("Default Java source directory.").withRank(ASetting)
|
||||
val sourceDirectories = settingKey[Seq[File]]("List of all source directories, both managed and unmanaged.").withRank(AMinusSetting)
|
||||
val unmanagedSourceDirectories = settingKey[Seq[File]]("Unmanaged source directories, which contain manually created sources.").withRank(ASetting)
|
||||
@transient
|
||||
val unmanagedSources = taskKey[Seq[File]]("Unmanaged sources, which are manually created.").withRank(BPlusTask)
|
||||
val managedSourceDirectories = settingKey[Seq[File]]("Managed source directories, which contain sources generated by the build.").withRank(BSetting)
|
||||
@transient
|
||||
val managedSources = taskKey[Seq[File]]("Sources generated by the build.").withRank(BTask)
|
||||
@transient
|
||||
val sources = taskKey[Seq[File]]("All sources, both managed and unmanaged.").withRank(BTask)
|
||||
val sourcesInBase = settingKey[Boolean]("If true, sources from the project's base directory are included as main sources.")
|
||||
|
||||
|
|
@ -171,10 +174,13 @@ object Keys {
|
|||
val resourceDirectory = settingKey[File]("Default unmanaged resource directory, used for user-defined resources.").withRank(ASetting)
|
||||
val resourceManaged = settingKey[File]("Default managed resource directory, used when generating resources.").withRank(BSetting)
|
||||
val unmanagedResourceDirectories = settingKey[Seq[File]]("Unmanaged resource directories, containing resources manually created by the user.").withRank(AMinusSetting)
|
||||
@transient
|
||||
val unmanagedResources = taskKey[Seq[File]]("Unmanaged resources, which are manually created.").withRank(BPlusTask)
|
||||
val managedResourceDirectories = settingKey[Seq[File]]("List of managed resource directories.").withRank(AMinusSetting)
|
||||
@transient
|
||||
val managedResources = taskKey[Seq[File]]("Resources generated by the build.").withRank(BTask)
|
||||
val resourceDirectories = settingKey[Seq[File]]("List of all resource directories, both managed and unmanaged.").withRank(BPlusSetting)
|
||||
@transient
|
||||
val resources = taskKey[Seq[File]]("All resource files, both managed and unmanaged.").withRank(BTask)
|
||||
private[sbt] val resourceDigests = taskKey[Seq[Digest]]("All resource files, both managed and unmanaged.").withRank(BTask)
|
||||
|
||||
|
|
@ -257,6 +263,7 @@ object Keys {
|
|||
val manipulateBytecode = taskKey[CompileResult]("Manipulates generated bytecode").withRank(BTask)
|
||||
val compileIncremental = taskKey[(Boolean, VirtualFileRef, HashedVirtualFileRef)]("Actually runs the incremental compilation").withRank(DTask)
|
||||
val previousCompile = taskKey[PreviousResult]("Read the incremental compiler analysis from disk").withRank(DTask)
|
||||
@transient
|
||||
val tastyFiles = taskKey[Seq[File]]("Returns the TASTy files produced by compilation").withRank(DTask)
|
||||
private[sbt] val compileScalaBackend = taskKey[CompileResult]("Compiles only Scala sources if pipelining is enabled. Compiles both Scala and Java sources otherwise").withRank(Invisible)
|
||||
private[sbt] val compileEarly = taskKey[CompileAnalysis]("Compiles only Scala sources if pipelining is enabled, and produce an early output (pickle JAR)").withRank(Invisible)
|
||||
|
|
@ -273,17 +280,21 @@ object Keys {
|
|||
val earlyCompileAnalysisTargetRoot = settingKey[File]("The output directory to produce Zinc Analysis files").withRank(DSetting)
|
||||
@transient
|
||||
val compileAnalysisFile = taskKey[File]("Zinc analysis storage.").withRank(DSetting)
|
||||
@transient
|
||||
val earlyCompileAnalysisFile = taskKey[File]("Zinc analysis storage for early compilation").withRank(DSetting)
|
||||
|
||||
@transient
|
||||
val compileIncSetup = taskKey[Setup]("Configures aspects of incremental compilation.").withRank(DTask)
|
||||
val compilerCache = taskKey[GlobalsCache]("Cache of scala.tools.nsc.Global instances. This should typically be cached so that it isn't recreated every task run.").withRank(DTask)
|
||||
val stateCompilerCache = AttributeKey[GlobalsCache]("stateCompilerCache", "Internal use: Global cache.")
|
||||
@transient
|
||||
val classpathEntryDefinesClass = taskKey[File => DefinesClass]("Internal use: provides a function that determines whether the provided file contains a given class.").withRank(Invisible)
|
||||
private[sbt] val classpathDefinesClassCache = settingKey[VirtualFileValueCache[DefinesClass]]("Internal use: a cache of jar classpath entries that persists across command evaluations.").withRank(Invisible)
|
||||
val persistJarClasspath = settingKey[Boolean]("Toggles whether or not to cache jar classpath entries between command evaluations")
|
||||
val classpathEntryDefinesClassVF = taskKey[VirtualFile => DefinesClass]("Internal use: provides a function that determines whether the provided file contains a given class.").withRank(Invisible)
|
||||
@transient
|
||||
val doc = taskKey[File]("Generates API documentation.").withRank(AMinusTask)
|
||||
@transient
|
||||
val copyResources = taskKey[Seq[(File, File)]]("Copies resources to the output directory.").withRank(AMinusTask)
|
||||
val aggregate = settingKey[Boolean]("Configures task aggregation.").withRank(BMinusSetting)
|
||||
val sourcePositionMappers = taskKey[Seq[xsbti.Position => Option[xsbti.Position]]]("Maps positions in generated source files to the original source it was generated from").withRank(DTask)
|
||||
|
|
@ -407,7 +418,9 @@ object Keys {
|
|||
val projectInfo = settingKey[ModuleInfo]("Addition project information like formal name, homepage, licenses etc.").withRank(CSetting)
|
||||
val defaultConfiguration = settingKey[Option[Configuration]]("Defines the configuration used when none is specified for a dependency in ivyXML.").withRank(CSetting)
|
||||
|
||||
@transient
|
||||
val products = taskKey[Seq[File]]("Build products that get packaged.").withRank(BMinusTask)
|
||||
@transient
|
||||
val productDirectories = taskKey[Seq[File]]("Base directories of build products.").withRank(CTask)
|
||||
val exportJars = settingKey[Boolean]("Determines whether the exported classpath for this project contains classes (false) or a packaged jar (true).").withRank(BSetting)
|
||||
val exportedProducts = taskKey[Classpath]("Build products that go on the exported classpath.").withRank(CTask)
|
||||
|
|
@ -539,6 +552,7 @@ object Keys {
|
|||
val updateClassifiers = TaskKey[UpdateReport]("updateClassifiers", "Resolves and optionally retrieves classified artifacts, such as javadocs and sources, for dependency definitions, transitively.", BPlusTask, update)
|
||||
val transitiveClassifiers = settingKey[Seq[String]]("List of classifiers used for transitively obtaining extra artifacts for sbt or declared dependencies.").withRank(BSetting)
|
||||
val updateSbtClassifiers = TaskKey[UpdateReport]("updateSbtClassifiers", "Resolves and optionally retrieves classifiers, such as javadocs and sources, for sbt, transitively.", BPlusTask, updateClassifiers)
|
||||
@transient
|
||||
val dependencyLock = taskKey[File]("Generates a dependency lock file from the current resolution.").withRank(BTask)
|
||||
val dependencyLockCheck = taskKey[Unit]("Checks if the dependency lock file is up-to-date.").withRank(BTask)
|
||||
val dependencyLockFile = settingKey[File]("The location of the dependency lock file.").withRank(CSetting)
|
||||
|
|
@ -562,9 +576,12 @@ object Keys {
|
|||
val allCredentials = taskKey[Seq[Credentials]]("Aggregated credentials across current and root subprojects. Do not rewire this task.").withRank(DTask)
|
||||
|
||||
val makePom = taskKey[HashedVirtualFileRef]("Generates a pom for publishing when publishing Maven-style.").withRank(BPlusTask)
|
||||
@transient
|
||||
val deliver = taskKey[File]("Generates the Ivy file for publishing to a repository.").withRank(BTask)
|
||||
@transient
|
||||
val deliverLocal = taskKey[File]("Generates the Ivy file for publishing to the local repository.").withRank(BTask)
|
||||
// makeIvyXml is currently identical to the confusingly-named "deliver", which may be deprecated in the future
|
||||
@transient
|
||||
val makeIvyXml = taskKey[File]("Generates the Ivy file for publishing to a repository.").withRank(BTask)
|
||||
/** BOM-resolved ModuleIDs for deps that had \"*\" (version from BOM); emitted as forced deps in published ivy.xml (sbt#4531). */
|
||||
val resolvedDependencies = taskKey[Seq[ModuleID]]("")
|
||||
|
|
@ -605,6 +622,7 @@ object Keys {
|
|||
val retrieveConfiguration = settingKey[Option[RetrieveConfiguration]]("Configures retrieving dependencies to the current build.").withRank(DSetting)
|
||||
val offline = settingKey[Boolean]("Configures sbt to work without a network connection where possible.").withRank(ASetting)
|
||||
val ivyPaths = settingKey[IvyPaths]("Configures paths used by Ivy for dependency management.").withRank(DSetting)
|
||||
@transient
|
||||
val dependencyCacheDirectory = taskKey[File]("The base directory for cached dependencies.").withRank(DTask)
|
||||
val libraryDependencies = settingKey[Seq[ModuleID]]("Declares managed dependencies.").withRank(APlusSetting)
|
||||
val dependencyOverrides = settingKey[Seq[ModuleID]]("Declares managed dependency overrides.").withRank(BSetting)
|
||||
|
|
@ -635,6 +653,7 @@ object Keys {
|
|||
val libraryDependencySchemes = settingKey[Seq[ModuleID]]("""Version scheme to use for specific modules set as "org" %% "name" % "<scheme>": Supported values are "early-semver", "pvp", "semver-spec", "always", and "strict".""").withRank(BSetting)
|
||||
@transient
|
||||
val stagingDirectory = settingKey[File]("Local staging directory for Sonatype publishing").withRank(CSetting)
|
||||
@transient
|
||||
val sonaBundle = taskKey[File]("Local bundle for Sonatype publishing").withRank(DTask)
|
||||
val localStaging = settingKey[Option[Resolver]]("Local staging resolver for Sonatype publishing").withRank(CSetting)
|
||||
val sonaDeploymentName = settingKey[String]("The name used for deployment").withRank(DSetting)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@transient
|
||||
val buildInfo = taskKey[Seq[File]]("generates the build info")
|
||||
|
||||
ThisBuild / scalaVersion := "2.12.21"
|
||||
|
|
|
|||
|
|
@ -1,36 +1,32 @@
|
|||
@transient
|
||||
val taskA = taskKey[File]("")
|
||||
@transient
|
||||
val taskB = taskKey[File]("")
|
||||
|
||||
@transient
|
||||
val taskE = taskKey[File]("")
|
||||
@transient
|
||||
val taskF = taskKey[File]("")
|
||||
|
||||
scalaVersion := "3.3.1"
|
||||
name := "task-map"
|
||||
taskA := {
|
||||
val c = fileConverter.value
|
||||
touch(target.value / "a")
|
||||
Def.declareOutput(c.toVirtualFile((target.value / "a").toPath()))
|
||||
target.value / "a"
|
||||
}
|
||||
|
||||
taskB := {
|
||||
val c = fileConverter.value
|
||||
touch(target.value / "b")
|
||||
Def.declareOutput(c.toVirtualFile((target.value / "b").toPath()))
|
||||
target.value / "b"
|
||||
}
|
||||
|
||||
taskE := {
|
||||
val c = fileConverter.value
|
||||
touch(target.value / "e")
|
||||
Def.declareOutput(c.toVirtualFile((target.value / "e").toPath()))
|
||||
target.value / "e"
|
||||
}
|
||||
|
||||
taskF := {
|
||||
val c = fileConverter.value
|
||||
touch(target.value / "f")
|
||||
Def.declareOutput(c.toVirtualFile((target.value / "f").toPath()))
|
||||
target.value / "f"
|
||||
}
|
||||
|
||||
|
|
@ -38,13 +34,13 @@ taskF := {
|
|||
// means "a" will be triggered by "b"
|
||||
// said differently, invoking "b" will run "b" and then run "a"
|
||||
|
||||
taskA := Def.uncached(taskA.triggeredBy(taskB).value)
|
||||
taskA := taskA.triggeredBy(taskB).value
|
||||
|
||||
// e <<= e runBefore f
|
||||
// means "e" will be run before running "f"
|
||||
// said differently, invoking "f" will run "e" and then run "f"
|
||||
|
||||
taskE := Def.uncached(taskE.runBefore(taskF).value)
|
||||
taskE := taskE.runBefore(taskF).value
|
||||
|
||||
// test utils
|
||||
def touch(f: File): File = { IO.touch(f); f }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import java.io.File
|
||||
|
||||
@transient
|
||||
val myFileTask = taskKey[File]("task that returns File")
|
||||
val badFileTask = taskKey[File]("task without @transient that should fail to cache")
|
||||
val checkFileTask = taskKey[Unit]("verifies file task returns correct value")
|
||||
|
||||
myFileTask := {
|
||||
new File(scalaVersion.value)
|
||||
}
|
||||
|
||||
checkFileTask := {
|
||||
val f = myFileTask.value
|
||||
val expected = new File(scalaVersion.value)
|
||||
assert(f == expected, s"Expected $expected but got $f")
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# @transient File-returning tasks should not be cached, so
|
||||
# changing scalaVersion should immediately be reflected.
|
||||
> checkFileTask
|
||||
> set scalaVersion := "2.13.18"
|
||||
> checkFileTask
|
||||
|
||||
# Using File as the return type of tasks without @transient should fail compilation.
|
||||
-> set badFileTask := { new java.io.File(scalaVersion.value) }
|
||||
|
|
@ -2,6 +2,7 @@ import sbt.internal.FileChangesMacro.inputFiles
|
|||
// The project contains two files: { Foo.txt, Bar.md } in the subdirector base/subdir/nested-subdir
|
||||
|
||||
// Check that we can correctly extract Foo.txt with a recursive source
|
||||
@transient
|
||||
val foo = taskKey[Seq[File]]("Retrieve Foo.txt")
|
||||
|
||||
foo / fileInputs += baseDirectory.value.toGlob / ** / "*.txt"
|
||||
|
|
@ -13,6 +14,7 @@ val checkFoo = taskKey[Unit]("Check that the Foo.txt file is retrieved")
|
|||
checkFoo := assert(foo.value == Seq(baseDirectory.value / "base/subdir/nested-subdir/Foo.txt"))
|
||||
|
||||
// Check that we can correctly extract Bar.md with a non-recursive source
|
||||
@transient
|
||||
val bar = taskKey[Seq[File]]("Retrieve Bar.md")
|
||||
|
||||
bar / fileInputs += baseDirectory.value.toGlob / "base" / "subdir" / "nested-subdir" / "*.md"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ val uTest = "com.lihaoyi" %% "utest" % "0.5.3"
|
|||
val foo = taskKey[Int]("")
|
||||
val bar = taskKey[Int]("")
|
||||
val baz = inputKey[Unit]("")
|
||||
@transient
|
||||
val buildInfo = taskKey[Seq[File]]("The task that generates the build info.")
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
|
|
|
|||
Loading…
Reference in New Issue