Reimplement clean

I ran into a couple of issues with the clean implementation. I changed
the logging to print to stdout instead of streams if enabled. I also
added a helper, Clean.deleteContents that recursively deletes all of the
contents of a directory except for those that match the exclude filter
parameter.

Using a normal logger was a bad idea because we are actually deleting
the target/streams directory when running clean.

The previous implementation worked by getting the full list of files to
delete, reverse sorting it and then deleting every element in the list.
While this can work well it many circumstances, if the directory is
still being written to during the recursive deletion, then we could miss
files that were added after we fetched all of the files. The new version
lazily lists the subdirectories as needed.
This commit is contained in:
Ethan Atkins 2019-02-06 14:17:01 -08:00
parent 16afe41cc1
commit b0c5e00c7c
2 changed files with 52 additions and 18 deletions

View File

@ -256,9 +256,9 @@ object Defaults extends BuildCommon {
skip :== false,
taskTemporaryDirectory := { val dir = IO.createTemporaryDirectory; dir.deleteOnExit(); dir },
onComplete := {
val dir = taskTemporaryDirectory.value;
val tempDirectory = taskTemporaryDirectory.value
() =>
{ IO.delete(dir); IO.createDirectory(dir) }
Clean.deleteContents(tempDirectory, _ => false)
},
useSuperShell :== sbt.internal.TaskProgress.isEnabled,
progressReports := { (s: State) =>

View File

@ -14,12 +14,31 @@ import java.nio.file.{ DirectoryNotEmptyException, Files }
import sbt.Def._
import sbt.Keys._
import sbt.Project.richInitializeTask
import sbt.internal.GlobLister._
import sbt.io.AllPassFilter
import sbt.io.syntax._
import sbt.io.{ AllPassFilter, FileTreeView, TypedPath }
import sbt.util.Level
object Clean {
def deleteContents(file: File, exclude: TypedPath => Boolean): Unit =
deleteContents(file, exclude, FileTreeView.DEFAULT, tryDelete((_: String) => {}))
def deleteContents(
file: File,
exclude: TypedPath => Boolean,
view: FileTreeView,
delete: File => Unit
): Unit = {
def deleteRecursive(file: File): Unit = {
view.list(file * AllPassFilter).filterNot(exclude).foreach {
case dir if dir.isDirectory =>
deleteRecursive(dir.toPath.toFile)
delete(dir.toPath.toFile)
case f => delete(f.toPath.toFile)
}
}
deleteRecursive(file)
}
/**
* Provides an implementation for the clean task. It delegates to [[taskIn]] using the
* resolvedScoped key to set the scope.
@ -41,22 +60,37 @@ object Clean {
case f if f.isDirectory => f * AllPassFilter
case f => f.toGlob
} ++ cleanKeepGlobs.value
val excludeFilter: File => Boolean = excludes.toFileFilter.accept
val globDeletions = (outputs in scope).value.unique.filterNot(excludeFilter)
val toDelete = cleanFiles.value.filterNot(excludeFilter) match {
case f @ Seq(_, _*) => (globDeletions ++ f).distinct
case _ => globDeletions
val excludeFilter: TypedPath => Boolean = excludes.toTypedPathFilter
val debug = (logLevel in scope).?.value.orElse(state.value.get(logLevel.key)) match {
case Some(Level.Debug) =>
(string: String) =>
println(s"[debug] $string")
case _ =>
(_: String) =>
{}
}
val logger = streams.value.log
toDelete.sorted.reverseIterator.foreach { f =>
logger.debug(s"clean -- deleting file $f")
try Files.deleteIfExists(f.toPath)
catch {
case _: DirectoryNotEmptyException =>
logger.debug(s"clean -- unable to delete non-empty directory $f")
case e: IOException =>
logger.debug(s"Caught unexpected exception $e deleting $f")
val delete = tryDelete(debug)
cleanFiles.value.sorted.reverseIterator.foreach(delete)
(outputs in scope).value.foreach { g =>
val filter: TypedPath => Boolean = {
val globFilter = g.toTypedPathFilter
tp =>
!globFilter(tp) || excludeFilter(tp)
}
deleteContents(g.base.toFile, filter, FileTreeView.DEFAULT, delete)
delete(g.base.toFile)
}
} tag Tags.Clean
private def tryDelete(debug: String => Unit): File => Unit = file => {
try {
debug(s"clean -- deleting file $file")
Files.deleteIfExists(file.toPath)
()
} catch {
case _: DirectoryNotEmptyException =>
debug(s"clean -- unable to delete non-empty directory $file")
case e: IOException =>
debug(s"Caught unexpected exception $e deleting $file")
}
}
}