diff --git a/main-settings/src/main/scala/sbt/std/KeyMacro.scala b/main-settings/src/main/scala/sbt/std/KeyMacro.scala index 2d93c047e..500ce4c25 100644 --- a/main-settings/src/main/scala/sbt/std/KeyMacro.scala +++ b/main-settings/src/main/scala/sbt/std/KeyMacro.scala @@ -12,70 +12,54 @@ import java.io.File import scala.quoted.* import scala.reflect.ClassTag -import sbt.util.OptJsonWriter +import sbt.util.{ NoJsonWriter, OptJsonWriter } +import sbt.internal.util.{ AttributeKey, KeyTag } private[sbt] object KeyMacro: - def settingKeyImpl[A1: Type]( - description: Expr[String] - )(using qctx: Quotes): Expr[SettingKey[A1]] = - keyImpl2[A1, SettingKey[A1]]("settingKey") { (name, mf, ojw) => - val n = Expr(name) - '{ - SettingKey[A1]($n, $description)($mf, $ojw) - } - } + def settingKeyImpl[A1: Type](description: Expr[String])(using Quotes): Expr[SettingKey[A1]] = + val name = definingValName(errorMsg("settingKey")) + val tag = '{ KeyTag.Setting[A1](${ summonRuntimeClass[A1] }) } + val ojw = Expr + .summon[OptJsonWriter[A1]] + .getOrElse(errorAndAbort(s"OptJsonWriter[A] not found for ${Type.show[A1]}")) + '{ SettingKey(AttributeKey($name, $description, Int.MaxValue)(using $tag, $ojw)) } - def taskKeyImpl[A1: Type](description: Expr[String])(using qctx: Quotes): Expr[TaskKey[A1]] = - keyImpl[A1, TaskKey[A1]]("taskKey") { (name, mf) => - val n = Expr(name) - '{ - TaskKey[A1]($n, $description)($mf) - } - } + def taskKeyImpl[A1: Type](description: Expr[String])(using Quotes): Expr[TaskKey[A1]] = + val name = definingValName(errorMsg("taskKey")) + val tag: Expr[KeyTag[Task[A1]]] = Type.of[A1] match + case '[Seq[a]] => + '{ KeyTag.SeqTask(${ summonRuntimeClass[a] }) } + case _ => '{ KeyTag.Task(${ summonRuntimeClass[A1] }) } + '{ TaskKey(AttributeKey($name, $description, Int.MaxValue)(using $tag, NoJsonWriter())) } - def inputKeyImpl[A1: Type](description: Expr[String])(using qctx: Quotes): Expr[InputKey[A1]] = - keyImpl[A1, InputKey[A1]]("inputKey") { (name, mf) => - val n = Expr(name) - '{ - InputKey[A1]($n, $description)($mf) - } - } + def inputKeyImpl[A1: Type](description: Expr[String])(using Quotes): Expr[InputKey[A1]] = + val name = definingValName(errorMsg("inputTaskKey")) + val tag: Expr[KeyTag[InputTask[A1]]] = '{ KeyTag.InputTask(${ summonRuntimeClass[A1] }) } + '{ InputKey(AttributeKey($name, $description, Int.MaxValue)(using $tag, NoJsonWriter())) } - private def keyImpl[A1: Type, A2: Type](methodName: String)( - f: (String, Expr[ClassTag[A1]]) => Expr[A2] - )(using qctx: Quotes): Expr[A2] = - val tpe = summon[Type[A1]] - f( - definingValName(errorMsg(methodName)), - Expr.summon[ClassTag[A1]].getOrElse(sys.error("ClassTag[A] not found for $tpe")) - ) + def projectImpl(using Quotes): Expr[Project] = + val name = definingValName(errorMsg2) + '{ Project($name, new File($name)) } - private def keyImpl2[A1: Type, A2: Type](methodName: String)( - f: (String, Expr[ClassTag[A1]], Expr[OptJsonWriter[A1]]) => Expr[A2] - )(using qctx: Quotes): Expr[A2] = - val tpe = summon[Type[A1]] - f( - definingValName(errorMsg(methodName)), - Expr.summon[ClassTag[A1]].getOrElse(sys.error("ClassTag[A] not found for $tpe")), - Expr.summon[OptJsonWriter[A1]].getOrElse(sys.error("OptJsonWriter[A] not found for $tpe")), - ) + private def summonRuntimeClass[A: Type](using Quotes): Expr[Class[?]] = + val classTag = Expr + .summon[ClassTag[A]] + .getOrElse(errorAndAbort(s"ClassTag[${Type.show[A]}] not found")) + '{ $classTag.runtimeClass } - def projectImpl(using qctx: Quotes): Expr[Project] = - val name = Expr(definingValName(errorMsg2("project"))) - '{ - Project($name, new File($name)) - } + private def errorAndAbort(msg: String)(using q: Quotes): Nothing = + q.reflect.report.errorAndAbort(msg) private def errorMsg(methodName: String): String = s"""$methodName must be directly assigned to a val, such as `val x = $methodName[Int]("description")`.""" - private def errorMsg2(methodName: String): String = - s"""$methodName must be directly assigned to a val, such as `val x = ($methodName in file("core"))`.""" + private def errorMsg2: String = + """project must be directly assigned to a val, such as `val x = project.in(file("core"))`.""" - private def definingValName(errorMsg: String)(using qctx: Quotes): String = + private def definingValName(errorMsg: String)(using qctx: Quotes): Expr[String] = val term = enclosingTerm - if term.isValDef then term.name - else sys.error(errorMsg) + if term.isValDef then Expr(term.name) + else errorAndAbort(errorMsg) def enclosingTerm(using qctx: Quotes) = import qctx.reflect._ diff --git a/main/src/main/scala/sbt/ScopedKeyData.scala b/main/src/main/scala/sbt/ScopedKeyData.scala index 76bafd558..c17701c9d 100644 --- a/main/src/main/scala/sbt/ScopedKeyData.scala +++ b/main/src/main/scala/sbt/ScopedKeyData.scala @@ -21,6 +21,7 @@ final case class ScopedKeyData[A](scoped: ScopedKey[A], value: Any) { def description: String = key.tag match case KeyTag.Task(typeArg) => s"Task: $typeArg" + case KeyTag.SeqTask(typeArg) => s"Task: Seq[$typeArg]" case KeyTag.InputTask(typeArg) => s"Input task: $typeArg" case KeyTag.Setting(typeArg) => s"Setting: $typeArg = $value" } diff --git a/main/src/main/scala/sbt/internal/Clean.scala b/main/src/main/scala/sbt/internal/Clean.scala index 15febbddb..e766a678a 100644 --- a/main/src/main/scala/sbt/internal/Clean.scala +++ b/main/src/main/scala/sbt/internal/Clean.scala @@ -24,6 +24,7 @@ import sbt.nio.file.Glob.{ GlobOps } import sbt.util.Level import sjsonnew.JsonFormat import scala.annotation.nowarn +import xsbti.{ PathBasedFile, VirtualFileRef } private[sbt] object Clean { @@ -142,8 +143,13 @@ private[sbt] object Clean { private[sbt] object ToSeqPath: given identitySeqPath: ToSeqPath[Seq[Path]] = identity[Seq[Path]](_) given seqFile: ToSeqPath[Seq[File]] = _.map(_.toPath) + given virtualFileRefSeq: ToSeqPath[Seq[VirtualFileRef]] = + _.collect { case f: PathBasedFile => f.toPath } given path: ToSeqPath[Path] = _ :: Nil given file: ToSeqPath[File] = _.toPath :: Nil + given virtualFileRef: ToSeqPath[VirtualFileRef] = + case f: PathBasedFile => Seq(f.toPath) + case _ => Nil end ToSeqPath private[this] implicit class ToSeqPathOps[T](val t: T) extends AnyVal { diff --git a/main/src/main/scala/sbt/nio/FileStamp.scala b/main/src/main/scala/sbt/nio/FileStamp.scala index 36d78d9c2..3146f92f5 100644 --- a/main/src/main/scala/sbt/nio/FileStamp.scala +++ b/main/src/main/scala/sbt/nio/FileStamp.scala @@ -16,6 +16,7 @@ import sbt.io.IO import sbt.nio.file.FileAttributes import sjsonnew.{ Builder, JsonFormat, Unbuilder, deserializationError } import xsbti.compile.analysis.{ Stamp => XStamp } +import xsbti.VirtualFileRef /** * A trait that indicates what file stamping implementation should be used to track the state of @@ -102,65 +103,49 @@ object FileStamp { private[sbt] final case class Error(exception: IOException) extends FileStamp object Formats { - implicit val seqPathJsonFormatter: JsonFormat[Seq[Path]] = new JsonFormat[Seq[Path]] { - override def write[J](obj: Seq[Path], builder: Builder[J]): Unit = { - builder.beginArray() - obj.foreach { path => - builder.writeString(path.toString) + implicit val seqPathJsonFormatter: JsonFormat[Seq[Path]] = + asStringArray(_.toString, Paths.get(_)) + implicit val seqFileJsonFormatter: JsonFormat[Seq[File]] = + asStringArray(_.toString, new File(_)) + implicit val seqVirtualFileRefJsonFormatter: JsonFormat[Seq[VirtualFileRef]] = + asStringArray(_.id, VirtualFileRef.of) + + implicit val fileJsonFormatter: JsonFormat[File] = fromSeqJsonFormat[File] + implicit val pathJsonFormatter: JsonFormat[Path] = fromSeqJsonFormat[Path] + implicit val virtualFileRefJsonFormatter: JsonFormat[VirtualFileRef] = + fromSeqJsonFormat[VirtualFileRef] + + private def asStringArray[T](toStr: T => String, fromStr: String => T): JsonFormat[Seq[T]] = + new JsonFormat[Seq[T]] { + override def write[J](obj: Seq[T], builder: Builder[J]): Unit = { + builder.beginArray() + obj.foreach { x => builder.writeString(toStr(x)) } + builder.endArray() } - builder.endArray() + + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Seq[T] = + jsOpt match { + case Some(js) => + val size = unbuilder.beginArray(js) + val res = (1 to size) map { _ => + fromStr(unbuilder.readString(unbuilder.nextElement)) + } + unbuilder.endArray() + res + case None => + deserializationError("Expected JsArray but found None") + } } - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Seq[Path] = - jsOpt match { - case Some(js) => - val size = unbuilder.beginArray(js) - val res = (1 to size) map { _ => - Paths.get(unbuilder.readString(unbuilder.nextElement)) - } - unbuilder.endArray() - res - case None => - deserializationError("Expected JsArray but found None") - } - } + private def fromSeqJsonFormat[T](using seqJsonFormat: JsonFormat[Seq[T]]): JsonFormat[T] = + new JsonFormat[T] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): T = + seqJsonFormat.read(jsOpt, unbuilder).head - implicit val seqFileJsonFormatter: JsonFormat[Seq[File]] = new JsonFormat[Seq[File]] { - override def write[J](obj: Seq[File], builder: Builder[J]): Unit = { - builder.beginArray() - obj.foreach { file => - builder.writeString(file.toString) - } - builder.endArray() + override def write[J](obj: T, builder: Builder[J]): Unit = + seqJsonFormat.write(obj :: Nil, builder) } - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Seq[File] = - jsOpt match { - case Some(js) => - val size = unbuilder.beginArray(js) - val res = (1 to size) map { _ => - new File(unbuilder.readString(unbuilder.nextElement)) - } - unbuilder.endArray() - res - case None => - deserializationError("Expected JsArray but found None") - } - } - implicit val fileJsonFormatter: JsonFormat[File] = new JsonFormat[File] { - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): File = - seqFileJsonFormatter.read(jsOpt, unbuilder).head - - override def write[J](obj: File, builder: Builder[J]): Unit = - seqFileJsonFormatter.write(obj :: Nil, builder) - } - implicit val pathJsonFormatter: JsonFormat[Path] = new JsonFormat[Path] { - override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): Path = - seqPathJsonFormatter.read(jsOpt, unbuilder).head - - override def write[J](obj: Path, builder: Builder[J]): Unit = - seqPathJsonFormatter.write(obj :: Nil, builder) - } implicit val seqPathFileStampJsonFormatter: JsonFormat[Seq[(Path, FileStamp)]] = new JsonFormat[Seq[(Path, FileStamp)]] { override def write[J](obj: Seq[(Path, FileStamp)], builder: Builder[J]): Unit = { diff --git a/main/src/main/scala/sbt/nio/Settings.scala b/main/src/main/scala/sbt/nio/Settings.scala index c39a4b909..f2e7488d6 100644 --- a/main/src/main/scala/sbt/nio/Settings.scala +++ b/main/src/main/scala/sbt/nio/Settings.scala @@ -25,6 +25,8 @@ import sjsonnew.JsonFormat import scala.annotation.nowarn import scala.collection.immutable.VectorBuilder +import java.io.File +import xsbti.VirtualFileRef private[sbt] object Settings { private[sbt] def inject(transformed: Seq[Def.Setting[_]]): Seq[Def.Setting[_]] = { @@ -68,45 +70,55 @@ private[sbt] object Settings { setting: Def.Setting[_], fileOutputScopes: Set[Scope] ): List[Def.Setting[_]] = { - val attributeKey = setting.key.key - attributeKey.tag match { + setting.key.key.tag match { case tag: KeyTag.Task[?] => - def default: List[Def.Setting[_]] = { - val scope = setting.key.scope.copy(task = Select(attributeKey)) - if (fileOutputScopes.contains(scope)) { - val sk = setting.asInstanceOf[Def.Setting[Task[Any]]].key - val scopedKey = Keys.dynamicFileOutputs in (sk.scope in sk.key) - addTaskDefinition { - val init: Def.Initialize[Task[Seq[Path]]] = sk(_.map(_ => Nil)) - Def.setting[Task[Seq[Path]]](scopedKey, init, setting.pos) - } :: allOutputPathsImpl(scope) :: outputFileStampsImpl(scope) :: cleanImpl(scope) :: Nil - } else Nil - } - def mkSetting[T: JsonFormat: ToSeqPath]: List[Def.Setting[_]] = { - val sk = setting.asInstanceOf[Def.Setting[Task[T]]].key - val taskKey = TaskKey(sk.key) in sk.scope - // We create a previous reference so that clean automatically works without the - // user having to explicitly call previous anywhere. - val init = Previous.runtime(taskKey).zip(taskKey) { case (_, t) => - t.map(implicitly[ToSeqPath[T]].apply) - } - val key = Def.ScopedKey(taskKey.scope in taskKey.key, Keys.dynamicFileOutputs.key) - addTaskDefinition(Def.setting[Task[Seq[Path]]](key, init, setting.pos)) :: - outputsAndStamps(taskKey) - } - if seqClass.isAssignableFrom(tag.typeArg) then - // TODO fix this: maybe using the taskKey macro to convey the information - // t.typeArguments match { - // case p :: Nil if pathClass.isAssignableFrom(p.runtimeClass) => mkSetting[Seq[Path]] - // case _ => default - // } - default - else if pathClass.isAssignableFrom(tag.typeArg) then mkSetting[Path] - else default + if pathClass.isAssignableFrom(tag.typeArg) then addOutputAndStampTasks[Path](setting) + else if fileClass.isAssignableFrom(tag.typeArg) then addOutputAndStampTasks[File](setting) + else if virtualFileRefClass.isAssignableFrom(tag.typeArg) then + addOutputAndStampTasks[VirtualFileRef](setting) + else addDefaultTasks(setting, fileOutputScopes) + case tag: KeyTag.SeqTask[?] => + if pathClass.isAssignableFrom(tag.typeArg) then addOutputAndStampTasks[Seq[Path]](setting) + else if fileClass.isAssignableFrom(tag.typeArg) then + addOutputAndStampTasks[Seq[File]](setting) + else if virtualFileRefClass.isAssignableFrom(tag.typeArg) then + addOutputAndStampTasks[Seq[VirtualFileRef]](setting) + else addDefaultTasks(setting, fileOutputScopes) case _ => Nil } } + @nowarn + private def addDefaultTasks( + setting: Def.Setting[_], + fileOutputScopes: Set[Scope] + ): List[Def.Setting[_]] = { + val scope = setting.key.scope.copy(task = Select(setting.key.key)) + if (fileOutputScopes.contains(scope)) { + val sk = setting.asInstanceOf[Def.Setting[Task[Any]]].key + val scopedKey = Keys.dynamicFileOutputs in (sk.scope in sk.key) + val init: Def.Initialize[Task[Seq[Path]]] = sk(_.map(_ => Nil)) + addTaskDefinition(Def.setting[Task[Seq[Path]]](scopedKey, init, setting.pos)) :: + allOutputPathsImpl(scope) :: outputFileStampsImpl(scope) :: cleanImpl(scope) :: Nil + } else Nil + } + + @nowarn + private def addOutputAndStampTasks[T: JsonFormat: ToSeqPath]( + setting: Def.Setting[_] + ): List[Def.Setting[_]] = { + val sk = setting.asInstanceOf[Def.Setting[Task[T]]].key + val taskKey = TaskKey(sk.key) in sk.scope + // We create a previous reference so that clean automatically works without the + // user having to explicitly call previous anywhere. + val init = Previous.runtime(taskKey).zip(taskKey) { case (_, t) => + t.map(implicitly[ToSeqPath[T]].apply) + } + val key = Def.ScopedKey(taskKey.scope in taskKey.key, Keys.dynamicFileOutputs.key) + addTaskDefinition(Def.setting[Task[Seq[Path]]](key, init, setting.pos)) :: + outputsAndStamps(taskKey) + } + private[sbt] val inject: Def.ScopedKey[_] => Seq[Def.Setting[_]] = scopedKey => scopedKey.key match { case transitiveDynamicInputs.key => @@ -161,7 +173,9 @@ private[sbt] object Settings { } private[this] val seqClass = classOf[Seq[_]] - private[this] val pathClass = classOf[java.nio.file.Path] + private[this] val pathClass = classOf[Path] + private val fileClass = classOf[File] + private val virtualFileRefClass = classOf[VirtualFileRef] /** * Returns all of the paths for the regular files described by a glob. Directories and hidden diff --git a/sbt-app/src/sbt-test/actions/compile-clean/test b/sbt-app/src/sbt-test/actions/compile-clean/test index a1289b6b1..14bce965a 100644 --- a/sbt-app/src/sbt-test/actions/compile-clean/test +++ b/sbt-app/src/sbt-test/actions/compile-clean/test @@ -6,17 +6,14 @@ $ exists target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class > Test/clean $ exists target/cant-touch-this -# TODO it should clean only test classes -# $ exists target/out/jvm/scala-2.12.17/compile-clean/classes/A.class -# $ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class +$ exists target/out/jvm/scala-2.12.17/compile-clean/classes/A.class +$ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class $ absent target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class # compiling everything again, but now cleaning only compile classes > Test/products > Compile/clean $ exists target/cant-touch-this -# TODO it should clean only compile classes $ absent target/out/jvm/scala-2.12.17/compile-clean/classes/A.class -# $ exists target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class -# TODO and X has to be kept, because of the cleanKeepFiles override -# $ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class +$ exists target/out/jvm/scala-2.12.17/compile-clean/test-classes/B.class +$ exists target/out/jvm/scala-2.12.17/compile-clean/classes/X.class diff --git a/util-collection/src/main/scala/sbt/internal/util/Attributes.scala b/util-collection/src/main/scala/sbt/internal/util/Attributes.scala index 48b0f0567..eaf34b8f1 100644 --- a/util-collection/src/main/scala/sbt/internal/util/Attributes.scala +++ b/util-collection/src/main/scala/sbt/internal/util/Attributes.scala @@ -15,11 +15,13 @@ import sjsonnew.* enum KeyTag[A]: case Setting[A](typeArg: Class[?]) extends KeyTag[A] case Task[A](typeArg: Class[?]) extends KeyTag[A] + case SeqTask[A](typeArg: Class[?]) extends KeyTag[A] case InputTask[A](typeArg: Class[?]) extends KeyTag[A] override def toString: String = this match case Setting(typeArg) => typeArg.toString case Task(typeArg) => s"Task[$typeArg]" + case SeqTask(typeArg) => s"Task[Seq[$typeArg]]" case InputTask(typeArg) => s"InputTask[$typeArg]" def typeArg: Class[?]