sbt.nio.Settings.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of main_2.12 Show documentation
Show all versions of main_2.12 Show documentation
sbt is an interactive build tool
The newest version!
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package nio
import java.nio.file.{ Files, Path }
import java.util.concurrent.ConcurrentHashMap
import sbt.Keys._
import sbt.internal.Clean.ToSeqPath
import sbt.internal.Continuous.FileStampRepository
import sbt.internal.util.AttributeKey
import sbt.internal.{ Clean, Continuous, DynamicInput, WatchTransitiveDependencies }
import sbt.nio.FileStamp.Formats._
import sbt.nio.FileStamper.{ Hash, LastModified }
import sbt.nio.Keys._
import sbt.nio.file.{ AllPass, FileAttributes, Glob, RecursiveGlob }
import sbt.std.TaskExtra._
import sjsonnew.JsonFormat
import scala.annotation.nowarn
import scala.collection.immutable.VectorBuilder
private[sbt] object Settings {
private[sbt] def inject(transformed: Seq[Def.Setting[_]]): Seq[Def.Setting[_]] = {
val definedSettings = new java.util.HashMap[Def.ScopedKey[_], VectorBuilder[Def.Setting[_]]]
val fileOutputScopes = transformed.flatMap { s =>
val list = new VectorBuilder[Def.Setting[_]]
definedSettings.putIfAbsent(s.key, list) match {
case null => list += s
case l => l += s
}
if (s.key.key == fileOutputs.key && s.key.scope.task.toOption.isDefined) Some(s.key.scope)
else None
}.toSet
transformed.flatMap { s =>
val inject =
if (s.key.key == fileInputs.key) inputPathSettings(s)
else maybeAddOutputsAndFileStamps(s, fileOutputScopes)
s +: inject.flatMap { setting =>
(definedSettings.get(setting.key) match {
case null => Vector(setting)
case set => setting +: set.result()
}): Seq[Def.Setting[_]]
}
}
}
/**
* This method checks if the setting is for a task with a return type in:
* `File`, `Seq[File]`, `Path`, `Seq[Path`. If it does, then we inject a number of
* task definition settings that allow the user to check if the output paths of
* the task have changed. It also adds a custom clean task that will delete the
* paths returned by the task, provided that they are in the task's target directory. We also
* inject these tasks if the fileOutputs setting is defined for the task.
*
* @param setting the setting to possibly inject with additional settings
* @param fileOutputScopes the set of scopes for which the fileOutputs setting is defined
* @return the injected settings
*/
@nowarn
private[this] def maybeAddOutputsAndFileStamps(
setting: Def.Setting[_],
fileOutputScopes: Set[Scope]
): List[Def.Setting[_]] = {
setting.key.key match {
case ak: AttributeKey[_] if taskClass.isAssignableFrom(ak.manifest.runtimeClass) =>
def default: List[Def.Setting[_]] = {
val scope = setting.key.scope.copy(task = Select(ak))
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)
}
ak.manifest.typeArguments match {
case t :: Nil if seqClass.isAssignableFrom(t.runtimeClass) =>
t.typeArguments match {
case p :: Nil if pathClass.isAssignableFrom(p.runtimeClass) => mkSetting[Seq[Path]]
case _ => default
}
case t :: Nil if pathClass.isAssignableFrom(t.runtimeClass) => mkSetting[Path]
case _ => default
}
case _ => Nil
}
}
@nowarn
private[sbt] val inject: Def.ScopedKey[_] => Seq[Def.Setting[_]] = scopedKey =>
scopedKey.key match {
case transitiveDynamicInputs.key =>
scopedKey.scope.task.toOption.toSeq.map { key =>
val updatedKey = Def.ScopedKey(scopedKey.scope.copy(task = Zero), key)
transitiveDynamicInputs in scopedKey.scope :=
WatchTransitiveDependencies.task(updatedKey).value
}
case dynamicDependency.key => (dynamicDependency in scopedKey.scope := { () }) :: Nil
case transitiveClasspathDependency.key =>
(transitiveClasspathDependency in scopedKey.scope := { () }) :: Nil
case _ => Nil
}
/**
* This adds the [[taskDefinitionKey]] to the work for each [[Task]]. Without
* this, the previous macro doesn't work correctly because [[Previous]] is unable to
* reference the task.
*
* @param setting the [[Def.Setting[_}]] for which we add the task definition
* @tparam T the generic type of the task (needed for type checking because [[Task]] is invariant)
* @return the setting with the task definition
*/
private[this] def addTaskDefinition[T](setting: Def.Setting[Task[T]]): Def.Setting[Task[T]] =
setting.mapInit((sk, task) => Task(task.info.set(taskDefinitionKey, sk), task.work))
/**
* Returns all of the paths described by a glob along with their basic file attributes.
* No additional filtering is performed.
*
* @param setting the setting whose fileInputs we are seeking
* @return a task definition that retrieves the file input files and their attributes scoped
* to a particular task.
*/
@nowarn
private[sbt] def inputPathSettings(setting: Def.Setting[_]): Seq[Def.Setting[_]] = {
val scopedKey = setting.key
val scope = scopedKey.scope
(Keys.allInputPathsAndAttributes in scope := {
val view = (fileTreeView in scope).value
val inputs = (fileInputs in scope).value
val stamper = (inputFileStamper in scope).value
val forceTrigger = (watchForceTriggerOnAnyChange in scope).value
val dynamicInputs = (Continuous.dynamicInputs in scope).value
// This makes watch work by ensuring that the input glob is registered with the
// repository used by the watch process.
state.value.get(globalFileTreeRepository).foreach { repo =>
inputs.foreach(repo.register(_).foreach(_.close()))
}
dynamicInputs.foreach(_ ++= inputs.map(g => DynamicInput(g, stamper, forceTrigger)))
view.list(inputs)
}) :: fileStamps(scopedKey) :: allFilesImpl(scope) :: changedInputFilesImpl(scope)
}
private[this] val taskClass = classOf[Task[_]]
private[this] val seqClass = classOf[Seq[_]]
private[this] val pathClass = classOf[java.nio.file.Path]
/**
* Returns all of the paths for the regular files described by a glob. Directories and hidden
* files are excluded.
*
* @param scope the key whose file inputs we are seeking
* @return a task definition that retrieves all of the input paths scoped to the input key.
*/
@nowarn
private[this] def allFilesImpl(scope: Scope): Def.Setting[_] = {
addTaskDefinition(Keys.allInputFiles in scope := {
val filter =
(fileInputIncludeFilter in scope).value && !(fileInputExcludeFilter in scope).value
(Keys.allInputPathsAndAttributes in scope).value.collect {
case (p, a) if filter.accept(p, a) => p
}
})
}
/**
* Returns all of the regular files whose stamp has changed since the last time the
* task was evaluated. The result includes new and modified files but not deleted
* files or files whose stamp has not changed since the previous run. Directories and hidden
* files are excluded
*
* @param scope the scope corresponding to the task whose fileInputs we are seeking
* @return a task definition that retrieves the changed input files scoped to the key.
*/
@nowarn
private[this] def changedInputFilesImpl(scope: Scope): List[Def.Setting[_]] =
changedFilesImpl(scope, changedInputFiles, inputFileStamps) ::
(watchForceTriggerOnAnyChange in scope := {
(watchForceTriggerOnAnyChange in scope).?.value match {
case Some(t) => t
case None => false
}
}) :: Nil
@nowarn
private[this] def changedFilesImpl(
scope: Scope,
changeKey: TaskKey[Seq[(Path, FileStamp)] => FileChanges],
stampKey: TaskKey[Seq[(Path, FileStamp)]]
): Def.Setting[_] =
addTaskDefinition(changeKey in scope := {
val current = (stampKey in scope).value
changedFiles(_, current)
})
private[sbt] def changedFiles(
previous: Seq[(Path, FileStamp)],
current: Seq[(Path, FileStamp)]
): FileChanges = {
val createdBuilder = new VectorBuilder[Path]
val deletedBuilder = new VectorBuilder[Path]
val modifiedBuilder = new VectorBuilder[Path]
val unmodifiedBuilder = new VectorBuilder[Path]
val seen = ConcurrentHashMap.newKeySet[Path]
val prevMap = new ConcurrentHashMap[Path, FileStamp]()
previous.foreach { case (k, v) => prevMap.put(k, v); () }
current.foreach {
case (path, currentStamp) =>
if (seen.add(path)) {
prevMap.remove(path) match {
case null => createdBuilder += path
case old => (if (old != currentStamp) modifiedBuilder else unmodifiedBuilder) += path
}
}
}
prevMap.forEach((p, _) => deletedBuilder += p)
val unmodified = unmodifiedBuilder.result()
val deleted = deletedBuilder.result()
val created = createdBuilder.result()
val modified = modifiedBuilder.result()
if (created.isEmpty && deleted.isEmpty && modified.isEmpty) {
FileChanges.unmodified(unmodifiedBuilder.result)
} else {
FileChanges(created, deleted, modified, unmodified)
}
}
/**
* Provides an automatically generated clean method for a task that provides fileOutputs.
*
* @param scope the scope to add the custom clean
* @return a task specific clean implementation
*/
@nowarn
private[sbt] def cleanImpl(scope: Scope): Def.Setting[_] = addTaskDefinition {
sbt.Keys.clean in scope := Clean.task(scope, full = false).value
}
/**
* Provides an automatically generated clean method for a task that provides fileOutputs.
*
* @param taskKey the task for which we add a custom clean implementation
* @return a task specific clean implementation
*/
@nowarn
private[sbt] def cleanImpl[T: JsonFormat: ToSeqPath](taskKey: TaskKey[T]): Def.Setting[_] = {
val taskScope = taskKey.scope in taskKey.key
addTaskDefinition(sbt.Keys.clean in taskScope := Def.taskDyn {
// the clean file task needs to run first because the previous cache gets blown away
// by the second task
Def.unit(Clean.cleanFileOutputTask(taskKey).value)
Clean.task(taskScope, full = false)
}.value)
}
/**
* Returns all of the regular files and the corresponding file stamps for the file inputs
* scoped to the input key. Directories and hidden files are excluded.
*
* @param scopedKey the key whose fileInputs we are seeking
* @return a task definition that retrieves the input files and their file stamps scoped to the
* input key.
*/
@nowarn
private[sbt] def fileStamps(scopedKey: Def.ScopedKey[_]): Def.Setting[_] = {
import sbt.internal.CompatParColls.Converters._
val scope = scopedKey.scope
addTaskDefinition(Keys.inputFileStamps in scope := {
val cache = (unmanagedFileStampCache in scope).value
val stamper = (Keys.inputFileStamper in scope).value
val stampFile: Path => Option[(Path, FileStamp)] =
state.value.get(globalFileTreeRepository) match {
case Some(repo: FileStampRepository) =>
(path: Path) =>
repo.putIfAbsent(path, stamper) match {
case (None, Some(s)) =>
cache.put(path, s)
Some(path -> s)
case _ => cache.getOrElseUpdate(path, stamper).map(path -> _)
}
case _ =>
(path: Path) => cache.getOrElseUpdate(path, stamper).map(path -> _)
}
val filter =
(fileInputIncludeFilter in scope).value && !(fileInputExcludeFilter in scope).value
(Keys.allInputPathsAndAttributes in scope).value.par.flatMap {
case (path, a) if filter.accept(path, a) => stampFile(path)
case _ => None
}.toVector
})
}
@nowarn
private[this] def outputsAndStamps[T: JsonFormat: ToSeqPath](
taskKey: TaskKey[T]
): List[Def.Setting[_]] = {
val scope = taskKey.scope in taskKey.key
val changes = changedFilesImpl(scope, changedOutputFiles, outputFileStamps) :: Nil
allOutputPathsImpl(scope) :: outputFileStampsImpl(scope) :: cleanImpl(taskKey) :: changes
}
@nowarn
private[this] def allOutputPathsImpl(scope: Scope): Def.Setting[_] =
addTaskDefinition(allOutputFiles in scope := {
val filter =
(fileOutputIncludeFilter in scope).value && !(fileOutputExcludeFilter in scope).value
val view = (fileTreeView in scope).value
val fileOutputGlobs = (fileOutputs in scope).value
val allFileOutputs = view.list(fileOutputGlobs).map(_._1)
val dynamicOutputs = (dynamicFileOutputs in scope).value
val allDynamicOutputs = dynamicOutputs.flatMap {
case p if Files.isDirectory(p) => p +: view.list(Glob(p, RecursiveGlob)).map(_._1)
case p => p :: Nil
}
/*
* We want to avoid computing the FileAttributes in the common case where nothing is
* being filtered (which is the case with the default filters:
* include = AllPass, exclude = NoPass).
*/
val attributeFilter: Path => Boolean = filter match {
case AllPass => _ => true
case f => p => FileAttributes(p).map(f.accept(p, _)).getOrElse(false)
}
allFileOutputs ++ allDynamicOutputs.filterNot { p =>
fileOutputGlobs.exists(_.matches(p)) || !attributeFilter(p)
}
})
@nowarn
private[this] def outputFileStampsImpl(scope: Scope): Def.Setting[_] =
addTaskDefinition(outputFileStamps in scope := {
val stamper: Path => Option[FileStamp] = (outputFileStamper in scope).value match {
case LastModified => FileStamp.lastModified
case Hash => FileStamp.hash
}
val allFiles = (allOutputFiles in scope).value
// The cache invalidation is specifically so that source formatters can run before
// the compile task and the file stamps seen by compile match the post-format stamps.
allFiles.foreach((unmanagedFileStampCache in scope).value.invalidate)
allFiles.flatMap(p => stamper(p).map(p -> _))
})
}