diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala index 4194d1866..8fe5afb65 100644 --- a/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala @@ -15,7 +15,7 @@ import sbt.util.{ Digest, Monad, } -import xsbti.VirtualFile +import xsbti.{ VirtualFile, VirtualFileRef } /** * Implementation of a macro that provides a direct syntax for applicative functors and monads. It @@ -337,7 +337,7 @@ trait Cont: }.asExprOf[HashWriter[A2]] else summonHashWriter[A2] val tagsExpr = '{ List(${ Varargs(tags.map(Expr[CacheLevelTag](_))) }: _*) } - val block = letOutput(outputs)(body) + val block = letOutput(outputs, cacheConfigExpr)(body) '{ given HashWriter[A2] = $inputHashWriter given JsonFormat[A1] = $aJsonFormat @@ -353,9 +353,28 @@ trait Cont: })($cacheConfigExpr) } - // wrap body in between output var declarations and var references + def toVirtualFileExpr( + cacheConfigExpr: Expr[BuildWideCacheConfiguration] + )(out: Output): Expr[VirtualFile] = + if out.isFile then out.toRef.asExprOf[VirtualFile] + else + '{ + ActionCache.packageDirectory( + dir = ${ out.toRef.asExprOf[VirtualFileRef] }, + conv = $cacheConfigExpr.fileConverter, + ) + } + + // This will generate following code for Def.declareOutput(...): + // var $o1: VirtualFile = null + // ActionCache.ActionResult({ + // body... + // $o1 = out // Def.declareOutput(out) + // result + // }, List($o1)) def letOutput[A1: Type]( - outputs: List[Output] + outputs: List[Output], + cacheConfigExpr: Expr[BuildWideCacheConfiguration], )(body: Expr[A1]): Expr[ActionCache.InternalActionResult[A1]] = Block( outputs.map(_.toVarDef), @@ -363,29 +382,38 @@ trait Cont: ActionCache.InternalActionResult( value = $body, outputs = List(${ - Varargs[VirtualFile](outputs.map(_.toRef.asExprOf[VirtualFile])) + Varargs[VirtualFile](outputs.map(toVirtualFileExpr(cacheConfigExpr))) }: _*), ) }.asTerm ).asExprOf[ActionCache.InternalActionResult[A1]] val WrapOutputName = "wrapOutput_\u2603\u2603" + val WrapOutputDirectoryName = "wrapOutputDirectory_\u2603\u2603" // Called when transforming the tree to add an input. // For `qual` of type F[A], and a `selection` qual.value. val record = [a] => (name: String, tpe: Type[a], qual: Term, oldTree: Term) => given t: Type[a] = tpe convert[a](name, qual) transform { (replacement: Term) => - if name != WrapOutputName then - // todo cache opt-out attribute - inputBuf += Input(TypeRepr.of[a], qual, replacement, freshName("q")) - oldTree - else - val output = Output(TypeRepr.of[a], qual, freshName("o"), Symbol.spliceOwner) - outputBuf += output - if cacheConfigExprOpt.isDefined then output.toAssign - else oldTree - end if + name match + case WrapOutputName | WrapOutputDirectoryName => + val output = Output( + tpe = TypeRepr.of[a], + term = qual, + name = freshName("o"), + parent = Symbol.spliceOwner, + outputType = name match + case WrapOutputName => OutputType.File + case WrapOutputDirectoryName => OutputType.Directory, + ) + outputBuf += output + if cacheConfigExprOpt.isDefined then output.toAssign + else oldTree + case _ => + // todo cache opt-out attribute + inputBuf += Input(TypeRepr.of[a], qual, replacement, freshName("q")) + oldTree } val exprWithConfig = cacheConfigExprOpt.map(config => '{ $config; $expr }).getOrElse(expr) diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala index 2e89f1cd1..c55716b02 100644 --- a/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala @@ -101,6 +101,10 @@ trait ContextUtil[C <: Quotes & scala.Singleton](val valStart: Int): case Apply(_, List(arg)) => extractTags(arg) case _ => extractTags0(tree) + enum OutputType: + case File + case Directory + /** * Represents an output expression via Def.declareOutput */ @@ -109,9 +113,10 @@ trait ContextUtil[C <: Quotes & scala.Singleton](val valStart: Int): val term: Term, val name: String, val parent: Symbol, + val outputType: OutputType, ): override def toString: String = - s"Output($tpe, $term, $name)" + s"Output($tpe, $term, $name, $outputType)" val placeholder: Symbol = tpe.asType match case '[a] => @@ -126,6 +131,7 @@ trait ContextUtil[C <: Quotes & scala.Singleton](val valStart: Int): ValDef(placeholder, rhs = Some('{ null }.asTerm)) def toAssign: Term = Assign(toRef, term) def toRef: Ref = Ref(placeholder) + def isFile: Boolean = outputType == OutputType.File end Output def applyTuple(tupleTerm: Term, tpe: TypeRepr, idx: Int): Term = diff --git a/main-settings/src/main/scala/sbt/Def.scala b/main-settings/src/main/scala/sbt/Def.scala index 6860077e7..3d534d9b5 100644 --- a/main-settings/src/main/scala/sbt/Def.scala +++ b/main-settings/src/main/scala/sbt/Def.scala @@ -9,7 +9,6 @@ package sbt import java.net.URI - import scala.annotation.tailrec import scala.annotation.targetName import sbt.KeyRanks.{ DTask, Invisible } @@ -20,7 +19,7 @@ import sbt.internal.util.{ Terminal => ITerminal, * } import sbt.util.{ ActionCacheStore, AggregateActionCacheStore, BuildWideCacheConfiguration, cacheLevel , DiskActionCacheStore } import Util._ import sbt.util.Show -import xsbti.{ HashedVirtualFileRef, VirtualFile } +import xsbti.{ HashedVirtualFileRef, VirtualFile, VirtualFileRef } import sjsonnew.JsonFormat import scala.reflect.ClassTag @@ -330,6 +329,8 @@ object Def extends Init[Scope] with TaskMacroExtra with InitializeImplicits: inline def declareOutput(inline vf: VirtualFile): Unit = InputWrapper.`wrapOutput_\u2603\u2603`[VirtualFile](vf) + inline def declareOutputDirectory(inline vf: VirtualFileRef): Unit = + InputWrapper.`wrapOutputDirectory_\u2603\u2603`[VirtualFileRef](vf) // The following conversions enable the types Initialize[T], Initialize[Task[T]], and Task[T] to // be used in task and setting macros as inputs with an ultimate result of type T diff --git a/main-settings/src/main/scala/sbt/std/InputConvert.scala b/main-settings/src/main/scala/sbt/std/InputConvert.scala index 98035c71b..f4229465c 100644 --- a/main-settings/src/main/scala/sbt/std/InputConvert.scala +++ b/main-settings/src/main/scala/sbt/std/InputConvert.scala @@ -74,12 +74,13 @@ class FullConvert[C <: Quotes & scala.Singleton](override val qctx: C, valStart: override def convert[A: Type](nme: String, in: Term): Converted = nme match - case InputWrapper.WrapInitTaskName => Converted.success(in) - case InputWrapper.WrapPreviousName => Converted.success(in) - case InputWrapper.WrapInitName => wrapInit[A](in) - case InputWrapper.WrapTaskName => wrapTask[A](in) - case InputWrapper.WrapOutputName => Converted.success(in) - case _ => Converted.NotApplicable() + case InputWrapper.WrapInitTaskName => Converted.success(in) + case InputWrapper.WrapPreviousName => Converted.success(in) + case InputWrapper.WrapInitName => wrapInit[A](in) + case InputWrapper.WrapTaskName => wrapTask[A](in) + case InputWrapper.WrapOutputName => Converted.success(in) + case InputWrapper.WrapOutputDirectoryName => Converted.success(in) + case _ => Converted.NotApplicable() private def wrapInit[A: Type](tree: Term): Converted = val expr = tree.asExprOf[Initialize[A]] diff --git a/main-settings/src/main/scala/sbt/std/InputWrapper.scala b/main-settings/src/main/scala/sbt/std/InputWrapper.scala index 553807e42..6e7a86758 100644 --- a/main-settings/src/main/scala/sbt/std/InputWrapper.scala +++ b/main-settings/src/main/scala/sbt/std/InputWrapper.scala @@ -22,6 +22,7 @@ object InputWrapper: private[std] final val WrapTaskName = "wrapTask_\u2603\u2603" private[std] final val WrapInitName = "wrapInit_\u2603\u2603" private[std] final val WrapOutputName = "wrapOutput_\u2603\u2603" + private[std] final val WrapOutputDirectoryName = "wrapOutputDirectory_\u2603\u2603" private[std] final val WrapInitTaskName = "wrapInitTask_\u2603\u2603" private[std] final val WrapInitInputName = "wrapInitInputTask_\u2603\u2603" private[std] final val WrapInputName = "wrapInputTask_\u2603\u2603" @@ -42,6 +43,11 @@ object InputWrapper: ) def `wrapOutput_\u2603\u2603`[A](@deprecated("unused", "") in: Any): A = implDetailError + @compileTimeOnly( + "`declareOutputDirectory` can only be used within a task macro, such as Def.cachedTask." + ) + def `wrapOutputDirectory_\u2603\u2603`[A](@deprecated("unused", "") in: Any): A = implDetailError + @compileTimeOnly( "`value` can only be called on a task within a task definition macro, such as :=, +=, ++=, or Def.task." ) diff --git a/util-cache/src/main/scala/sbt/util/ActionCache.scala b/util-cache/src/main/scala/sbt/util/ActionCache.scala index 62b1c8a70..4f8ec9849 100644 --- a/util-cache/src/main/scala/sbt/util/ActionCache.scala +++ b/util-cache/src/main/scala/sbt/util/ActionCache.scala @@ -1,5 +1,7 @@ package sbt.util +import java.io.File +import java.nio.file.Paths import sbt.internal.util.{ ActionCacheEvent, CacheEventLog, StringVirtualFile1 } import sbt.io.IO import scala.reflect.ClassTag @@ -7,7 +9,7 @@ import scala.annotation.{ meta, StaticAnnotation } import sjsonnew.{ HashWriter, JsonFormat } import sjsonnew.support.murmurhash.Hasher import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser } -import xsbti.{ FileConverter, VirtualFile } +import xsbti.{ FileConverter, VirtualFile, VirtualFileRef } import java.nio.charset.StandardCharsets import java.nio.file.Path import scala.quoted.{ Expr, FromExpr, ToExpr, Quotes } @@ -91,6 +93,16 @@ object ActionCache: else valueFromStr(IO.read(paths.head.toFile()), result.origin) case Left(_) => organicTask + def packageDirectory(dir: VirtualFileRef, conv: FileConverter): VirtualFile = + import sbt.io.syntax.* + val dirPath = conv.toPath(dir) + val dirFile = dirPath.toFile() + val zipPath = Paths.get(dirPath.toString + ".dirzip") + val rebase: File => Seq[(File, String)] = + f => if f != dirFile then (f -> dirPath.relativize(f.toPath).toString) :: Nil else Nil + IO.zip(dirFile.allPaths.get().flatMap(rebase), zipPath.toFile(), None) + conv.toVirtualFile(zipPath) + /** * Represents a value and output files, used internally by the macro. */