From b0c5e00c7c852bda1799c28b6a4495c4f794e9bb Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 6 Feb 2019 14:17:01 -0800 Subject: [PATCH] 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. --- main/src/main/scala/sbt/Defaults.scala | 4 +- main/src/main/scala/sbt/internal/Clean.scala | 66 +++++++++++++++----- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index cac5e7338..f2646f50a 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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) => diff --git a/main/src/main/scala/sbt/internal/Clean.scala b/main/src/main/scala/sbt/internal/Clean.scala index f66323e74..83629d505 100644 --- a/main/src/main/scala/sbt/internal/Clean.scala +++ b/main/src/main/scala/sbt/internal/Clean.scala @@ -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") + } + } }