Move deleteTree and deleteTreesBelow into FileSystem and Path.

The current implementation of these functions is very inefficient and
degrades overall performance significantly, especially when sandboxing is
enabled. However, that's almost the best we can do with a generic
algorithm.

To make room for optimizations that rely on specific file system features,
move these functions into the FileSystem class. I will supply a custom
implementation for UnixFileSystem later.

Note that this is intended to be a pure code move. I haven't applied any
improvements to the code nor tests yet (with the exception of cleaning up
docstrings).

Addresses https://github.com/bazelbuild/bazel/issues/7527.

RELNOTES: None.
PiperOrigin-RevId: 239412965
diff --git a/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
index 87e7629..a0e81de 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/AbstractAction.java
@@ -38,7 +38,6 @@
 import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.Root;
 import com.google.devtools.build.lib.vfs.Symlinks;
@@ -413,7 +412,7 @@
         parentDir.setWritable(true);
         deleteOutput(fileSystem, output);
       } else if (path.isDirectory(Symlinks.NOFOLLOW)) {
-        FileSystemUtils.deleteTree(path);
+        path.deleteTree();
       } else {
         throw e;
       }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
index 12e53c2..2bf8670 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
@@ -461,9 +461,9 @@
     execRoot.getRelative(unusedRunfilesLogPath).delete();
     // Note that splitLogsPath points to a file inside the splitLogsDir so
     // it's not necessary to delete it explicitly.
-    FileSystemUtils.deleteTree(execRoot.getRelative(splitLogsDir));
-    FileSystemUtils.deleteTree(execRoot.getRelative(undeclaredOutputsDir));
-    FileSystemUtils.deleteTree(execRoot.getRelative(undeclaredOutputsAnnotationsDir));
+    execRoot.getRelative(splitLogsDir).deleteTree();
+    execRoot.getRelative(undeclaredOutputsDir).deleteTree();
+    execRoot.getRelative(undeclaredOutputsAnnotationsDir).deleteTree();
     execRoot.getRelative(testStderr).delete();
     execRoot.getRelative(testExitSafe).delete();
     if (testShard != null) {
@@ -490,7 +490,7 @@
       // entries, which prevent removing the directory.  As a workaround, code below will throw
       // IOException if it will fail to remove something inside testAttemptsDir, but will
       // silently suppress any exceptions when deleting testAttemptsDir itself.
-      FileSystemUtils.deleteTreesBelow(testAttemptsDir);
+      testAttemptsDir.deleteTreesBelow();
       try {
         testAttemptsDir.delete();
       } catch (IOException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryFunction.java
index 62e8703..cd086d1 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryFunction.java
@@ -34,7 +34,6 @@
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Mutability;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
@@ -170,7 +169,7 @@
         // A dependency is missing, cleanup and returns null
         try {
           if (outputDirectory.exists()) {
-            FileSystemUtils.deleteTree(outputDirectory);
+            outputDirectory.deleteTree();
           }
         } catch (IOException e1) {
           throw new RepositoryFunctionException(e1, Transience.TRANSIENT);
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
index 05d8cb8..d648150 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
@@ -465,7 +465,7 @@
     Path directory = env.getActionConsoleOutputDirectory();
     try {
       if (directory.exists()) {
-        FileSystemUtils.deleteTree(directory);
+        directory.deleteTree();
       }
       FileSystemUtils.createDirectoryAndParents(directory);
     } catch (IOException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java b/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
index 1111adc..5975785 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
@@ -94,7 +94,7 @@
           continue dirloop;
         }
       }
-      FileSystemUtils.deleteTree(p);
+      p.deleteTree();
     }
   }
 
@@ -110,7 +110,7 @@
     Path realWorkspaceDir = execroot.getParentDirectory().getRelative(workspaceName);
     if (!workspaceName.equals(execroot.getBaseName()) && realWorkspaceDir.exists()
         && !realWorkspaceDir.isSymbolicLink()) {
-      FileSystemUtils.deleteTree(realWorkspaceDir);
+      realWorkspaceDir.deleteTree();
     }
 
     // Packages come from exactly one root, but their shared ancestors may come from more.
diff --git a/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
index 4086b4d..a0070c2 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/TestStrategy.java
@@ -41,7 +41,6 @@
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.io.FileWatcher;
 import com.google.devtools.build.lib.util.io.OutErr;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
@@ -86,7 +85,7 @@
 
   /** Removes directory if it exists and recreates it. */
   private void recreateDirectory(Path directory) throws IOException {
-    FileSystemUtils.deleteTree(directory);
+    directory.deleteTree();
     directory.createDirectoryAndParents();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
index 04f975d..cd55665 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
@@ -45,7 +45,6 @@
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.OsUtils;
 import com.google.devtools.build.lib.util.io.FileOutErr;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import java.io.File;
 import java.io.IOException;
@@ -398,7 +397,7 @@
         // File deletion tends to be slow on Windows, so deleting this tree may take several
         // seconds. Delete it after having measured the wallTime.
         try {
-          FileSystemUtils.deleteTree(tmpDir);
+          tmpDir.deleteTree();
         } catch (IOException ignored) {
           // We can't handle this exception in any meaningful way, nor should we, but let's log it.
           stepLog(
diff --git a/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java b/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java
index edbcf6e..0523929 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/AbstractRemoteActionCache.java
@@ -233,7 +233,7 @@
         for (OutputDirectory directory : result.getOutputDirectoriesList()) {
           // Only delete the directories below the output directories because the output
           // directories will not be re-created
-          FileSystemUtils.deleteTreesBelow(execRoot.getRelative(directory.getPath()));
+          execRoot.getRelative(directory.getPath()).deleteTreesBelow();
         }
         if (outErr != null) {
           outErr.getOutputPath().delete();
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
index 92eff2f..1a66585 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
@@ -41,7 +41,6 @@
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.io.AsynchronousFileOutputStream;
 import com.google.devtools.build.lib.vfs.DigestHashFunction;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.common.options.OptionsBase;
 import com.google.devtools.common.options.OptionsParsingResult;
@@ -292,7 +291,7 @@
     try {
       // Clean out old logs files.
       if (logDir.exists()) {
-        FileSystemUtils.deleteTree(logDir);
+        logDir.deleteTree();
       }
       logDir.createDirectory();
     } catch (IOException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkAction.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkAction.java
index b006593..6004f61 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CreateIncSymlinkAction.java
@@ -29,7 +29,6 @@
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.Symlinks;
 import java.io.IOException;
@@ -58,7 +57,7 @@
   @Override
   public void prepare(FileSystem fileSystem, Path execRoot) throws IOException {
     if (includePath.isDirectory(Symlinks.NOFOLLOW)) {
-      FileSystemUtils.deleteTree(includePath);
+      includePath.deleteTree();
     }
     super.prepare(fileSystem, execRoot);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java
index a9dc2f5..a4161cc 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryDelegatorFunction.java
@@ -111,7 +111,7 @@
 
   private void setupRepositoryRoot(Path repoRoot) throws RepositoryFunctionException {
     try {
-      FileSystemUtils.deleteTree(repoRoot);
+      repoRoot.deleteTree();
       Preconditions.checkNotNull(repoRoot.getParentDirectory()).createDirectoryAndParents();
     } catch (IOException e) {
       throw new RepositoryFunctionException(e, Transience.TRANSIENT);
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java
index 54d04cf..7a5f770 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeWorkspace.java
@@ -230,7 +230,7 @@
       actionCache.clear();
     }
     actionCache = null;
-    FileSystemUtils.deleteTree(getCacheDirectory());
+    getCacheDirectory().deleteTree();
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
index 1b0af99..957ae3e 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/CleanCommand.java
@@ -29,7 +29,6 @@
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.ProcessUtils;
 import com.google.devtools.build.lib.util.ShellEscaper;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionDocumentationCategory;
@@ -231,8 +230,8 @@
       // and links right before we exit. Once the lock file is gone there will
       // be a small possibility of a server race if a client is waiting, but
       // all significant files will be gone by then.
-      FileSystemUtils.deleteTreesBelow(outputBase);
-      FileSystemUtils.deleteTree(outputBase);
+      outputBase.deleteTreesBelow();
+      outputBase.deleteTree();
     } else if (expunge && async) {
       logger.info("Expunging asynchronously...");
       env.getRuntime().prepareForAbruptShutdown();
@@ -246,7 +245,7 @@
         if (async) {
           asyncClean(env, execroot, "Output tree");
         } else {
-          FileSystemUtils.deleteTreesBelow(execroot);
+          execroot.deleteTreesBelow();
         }
       }
     }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractContainerizingSandboxedSpawn.java b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractContainerizingSandboxedSpawn.java
index e3e1696..f902cfa 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractContainerizingSandboxedSpawn.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractContainerizingSandboxedSpawn.java
@@ -189,7 +189,7 @@
   @Override
   public void delete() {
     try {
-      FileSystemUtils.deleteTree(sandboxPath);
+      sandboxPath.deleteTree();
     } catch (IOException e) {
       // This usually means that the Spawn itself exited, but still has children running that
       // we couldn't wait for, which now block deletion of the sandbox directory. On Linux this
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
index 5777c44..d368a08 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
@@ -43,7 +43,6 @@
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.common.options.OptionsBase;
@@ -145,7 +144,7 @@
       sandboxfsProcess = null;
     }
     if (sandboxBase.exists()) {
-      FileSystemUtils.deleteTree(sandboxBase);
+      sandboxBase.deleteTree();
     }
 
     sandboxBase.createDirectoryAndParents();
@@ -368,7 +367,7 @@
 
     if (shouldCleanupSandboxBase) {
       try {
-        FileSystemUtils.deleteTree(sandboxBase);
+        sandboxBase.deleteTree();
       } catch (IOException e) {
         env.getReporter().handle(Event.warn("Failed to delete sandbox base " + sandboxBase
             + ": " + e));
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsSandboxedSpawn.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsSandboxedSpawn.java
index 0ed138a..44e27c3 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsSandboxedSpawn.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxfsSandboxedSpawn.java
@@ -184,7 +184,7 @@
     }
 
     try {
-      FileSystemUtils.deleteTree(sandboxPath);
+      sandboxPath.deleteTree();
     } catch (IOException e) {
       // This usually means that the Spawn itself exited but still has children running that
       // we couldn't wait for, which now block deletion of the sandbox directory.  (Those processes
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
index 9187c32..ad38ee5 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
@@ -200,6 +200,36 @@
   public abstract boolean delete(Path path) throws IOException;
 
   /**
+   * Deletes all directory trees recursively beneath the given path and removes that path as well.
+   *
+   * @param path the directory hierarchy to remove
+   * @throws IOException if the hierarchy cannot be removed successfully
+   */
+  public void deleteTree(Path path) throws IOException {
+    deleteTreesBelow(path);
+    path.delete();
+  }
+
+  /**
+   * Deletes all directory trees recursively beneath the given path. Does nothing if the given path
+   * is not a directory.
+   *
+   * @param dir the directory hierarchy to remove
+   * @throws IOException if the hierarchy cannot be removed successfully
+   */
+  public void deleteTreesBelow(Path dir) throws IOException {
+    if (dir.isDirectory(Symlinks.NOFOLLOW)) {
+      dir.setReadable(true);
+      dir.setWritable(true);
+      dir.setExecutable(true);
+      for (Path child : dir.getDirectoryEntries()) {
+        deleteTreesBelow(child);
+        child.delete();
+      }
+    }
+  }
+
+  /**
    * Returns the last modification time of the file denoted by {@code path}. See {@link
    * Path#getLastModifiedTime(Symlinks)} for specification.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
index b290eb8..322878f 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
@@ -31,11 +31,8 @@
 import java.util.Collection;
 import java.util.List;
 
-/**
- * Helper functions that implement often-used complex operations on file
- * systems.
- */
-@ConditionallyThreadSafe // ThreadSafe except for deleteTree.
+/** Helper functions that implement often-used complex operations on file systems. */
+@ConditionallyThreadSafe
 public class FileSystemUtils {
 
   private FileSystemUtils() {}
@@ -348,7 +345,7 @@
       /* fallthru and do the work below */
     }
     if (link.isSymbolicLink()) {
-      link.delete();  // Remove the symlink since it is pointing somewhere else.
+      link.delete(); // Remove the symlink since it is pointing somewhere else.
     } else {
       createDirectoryAndParents(link.getParentDirectory());
     }
@@ -538,36 +535,6 @@
   }
 
   /**
-   * Deletes 'p', and everything recursively beneath it if it's a directory.
-   * Does not follow any symbolic links.
-   *
-   * @throws IOException if any file could not be removed.
-   */
-  @ThreadSafe
-  public static void deleteTree(Path p) throws IOException {
-    deleteTreesBelow(p);
-    p.delete();
-  }
-
-  /**
-   * Deletes all dir trees recursively beneath 'dir' if it's a directory,
-   * nothing otherwise. Does not follow any symbolic links.
-   *
-   * @throws IOException if any file could not be removed.
-   */
-  @ThreadSafe
-  public static void deleteTreesBelow(Path dir) throws IOException {
-    if (dir.isDirectory(Symlinks.NOFOLLOW)) {  // real directories (not symlinks)
-      dir.setReadable(true);
-      dir.setWritable(true);
-      dir.setExecutable(true);
-      for (Path child : dir.getDirectoryEntries()) {
-        deleteTree(child);
-      }
-    }
-  }
-
-  /**
    * Copies all dir trees under a given 'from' dir to location 'to', while overwriting all files in
    * the potentially existing 'to'. Resolves symbolic links if {@code followSymlinks ==
    * Symlinks#FOLLOW}. Otherwise copies symlinks as-is.
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Path.java b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
index 5dc4c5b..18851ea 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/Path.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
@@ -687,6 +687,25 @@
   }
 
   /**
+   * Deletes all directory trees recursively beneath this path and removes the path as well.
+   *
+   * @throws IOException if the hierarchy cannot be removed successfully
+   */
+  public void deleteTree() throws IOException {
+    fileSystem.deleteTree(this);
+  }
+
+  /**
+   * Deletes all directory trees recursively beneath this path. Does nothing if the path is not a
+   * directory.
+   *
+   * @throws IOException if the hierarchy cannot be removed successfully
+   */
+  public void deleteTreesBelow() throws IOException {
+    fileSystem.deleteTreesBelow(this);
+  }
+
+  /**
    * Returns the last modification time of the file, in milliseconds since the UNIX epoch, of the
    * file denoted by the current path, following symbolic links.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java
index 615fc6f..46d64db 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java
@@ -15,7 +15,6 @@
 package com.google.devtools.build.lib.worker;
 
 import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.IOException;
@@ -35,7 +34,7 @@
   @Override
   void destroy() throws IOException {
     super.destroy();
-    FileSystemUtils.deleteTree(workDir);
+    workDir.deleteTree();
   }
 
   @Override
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCacheTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCacheTest.java
index 35ce53c..029c2d5 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/cache/RepositoryCacheTest.java
@@ -60,7 +60,7 @@
 
   @After
   public void tearDown() throws IOException {
-    FileSystemUtils.deleteTree(repositoryCachePath);
+    repositoryCachePath.deleteTree();
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
index cd24422..f58c04c 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
@@ -234,7 +234,7 @@
 
     fileSystem = FileSystems.getNativeFileSystem();
     testRoot = fileSystem.getPath(TestUtils.tmpDir());
-    FileSystemUtils.deleteTreesBelow(testRoot);
+    testRoot.deleteTreesBelow();
     executorService = Executors.newCachedThreadPool();
     inputArtifact =
         new Artifact(
diff --git a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
index 764bb8f..569dbb7 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/GlobCacheTest.java
@@ -22,7 +22,6 @@
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.util.Pair;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -111,7 +110,7 @@
 
   @After
   public final void deleteFiles() throws Exception  {
-    FileSystemUtils.deleteTreesBelow(scratch.getFileSystem().getPath("/"));
+    scratch.getFileSystem().getPath("/").deleteTreesBelow();
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/MockToolsConfig.java b/src/test/java/com/google/devtools/build/lib/packages/util/MockToolsConfig.java
index c4af0c6..b76bbf1 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/MockToolsConfig.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/MockToolsConfig.java
@@ -93,7 +93,7 @@
   public Path overwrite(String relativePath, String... lines) throws IOException {
     Path path = rootDirectory.getRelative(relativePath);
     if (path.exists()) {
-      FileSystemUtils.deleteTree(path);
+      path.deleteTree();
     }
     return create(relativePath, lines);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java b/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java
index 7cb82ad..23360f6 100644
--- a/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/pkgcache/LoadingPhaseRunnerTest.java
@@ -1083,7 +1083,7 @@
               workspace,
               /* defaultSystemJavabase= */ null,
               analysisMock.getProductName());
-      FileSystemUtils.deleteTree(workspace.getRelative("base"));
+      workspace.getRelative("base").deleteTree();
 
       ConfiguredRuleClassProvider ruleClassProvider = analysisMock.createRuleClassProvider();
       PackageFactory pkgFactory =
diff --git a/src/test/java/com/google/devtools/build/lib/query2/engine/PostAnalysisQueryHelper.java b/src/test/java/com/google/devtools/build/lib/query2/engine/PostAnalysisQueryHelper.java
index 2423b2d..6563664 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/engine/PostAnalysisQueryHelper.java
+++ b/src/test/java/com/google/devtools/build/lib/query2/engine/PostAnalysisQueryHelper.java
@@ -133,7 +133,7 @@
 
   @Override
   public void clearAllFiles() throws IOException {
-    FileSystemUtils.deleteTree(analysisHelper.getRootDirectory());
+    analysisHelper.getRootDirectory().deleteTree();
   }
 
   @Override
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java b/src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java
index 4754032..9f5fa0c 100644
--- a/src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/BaseSandboxfsProcessTest.java
@@ -54,7 +54,7 @@
 
   @After
   public void tearDown() throws IOException {
-    FileSystemUtils.deleteTreesBelow(tmpDir);
+    tmpDir.deleteTreesBelow();
     tmpDir = null;
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java b/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java
index ec3f713a..86cdf42 100644
--- a/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java
+++ b/src/test/java/com/google/devtools/build/lib/sandbox/FakeSandboxfsProcess.java
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.IOException;
@@ -114,6 +113,6 @@
 
     checkState(mapping.isAbsolute(), "Mapping specifications are expected to be absolute"
         + " but %s is not", mapping);
-    FileSystemUtils.deleteTree(fileSystem.getPath(mountPoint).getRelative(mapping.toRelative()));
+    fileSystem.getPath(mountPoint).getRelative(mapping.toRelative()).deleteTree();
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
index d2016cd..79cd067 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
@@ -519,7 +519,7 @@
 
     // We're done fiddling with this... restore the original state
     outEmpty.getPath().delete();
-    FileSystemUtils.deleteTree(dummyEmptyDir);
+    dummyEmptyDir.deleteTree();
     FileSystemUtils.createDirectoryAndParents(outEmpty.getPath());
 
     /* **** Tests for files and directory contents ****/
diff --git a/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
index 2dbcced..42cb2c6 100644
--- a/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategyTest.java
@@ -53,7 +53,6 @@
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.util.FileSystems;
 import com.google.devtools.common.options.Options;
@@ -89,7 +88,7 @@
     fileSystem = FileSystems.getNativeFileSystem();
     Path testRoot = fileSystem.getPath(TestUtils.tmpDir());
     try {
-      FileSystemUtils.deleteTreesBelow(testRoot);
+      testRoot.deleteTreesBelow();
     } catch (IOException e) {
       System.err.println("Failed to remove directory " + testRoot + ": " + e.getMessage());
       throw e;
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
index b34c2f5..4bda98a 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
@@ -802,6 +802,87 @@
     assertThat(xNonEmptyDirectoryFoo.isFile()).isTrue();
   }
 
+  @Test
+  public void testDeleteTreeCommandDeletesTree() throws IOException {
+    Path topDir = absolutize("top-dir");
+    Path file1 = absolutize("top-dir/file-1");
+    Path file2 = absolutize("top-dir/file-2");
+    Path aDir = absolutize("top-dir/a-dir");
+    Path file3 = absolutize("top-dir/a-dir/file-3");
+    Path file4 = absolutize("file-4");
+
+    topDir.createDirectory();
+    FileSystemUtils.createEmptyFile(file1);
+    FileSystemUtils.createEmptyFile(file2);
+    aDir.createDirectory();
+    FileSystemUtils.createEmptyFile(file3);
+    FileSystemUtils.createEmptyFile(file4);
+
+    Path toDelete = topDir;
+    toDelete.deleteTree();
+
+    assertThat(file4.exists()).isTrue();
+    assertThat(topDir.exists()).isFalse();
+    assertThat(file1.exists()).isFalse();
+    assertThat(file2.exists()).isFalse();
+    assertThat(aDir.exists()).isFalse();
+    assertThat(file3.exists()).isFalse();
+  }
+
+  @Test
+  public void testDeleteTreeCommandsDeletesUnreadableDirectories() throws IOException {
+    Path topDir = absolutize("top-dir");
+    Path aDir = absolutize("top-dir/a-dir");
+
+    topDir.createDirectory();
+    aDir.createDirectory();
+
+    Path toDelete = topDir;
+
+    try {
+      aDir.setReadable(false);
+    } catch (UnsupportedOperationException e) {
+      // For file systems that do not support setting readable attribute to
+      // false, this test is simply skipped.
+
+      return;
+    }
+
+    toDelete.deleteTree();
+    assertThat(topDir.exists()).isFalse();
+    assertThat(aDir.exists()).isFalse();
+  }
+
+  @Test
+  public void testDeleteTreeCommandDoesNotFollowLinksOut() throws IOException {
+    Path topDir = absolutize("top-dir");
+    Path file1 = absolutize("top-dir/file-1");
+    Path file2 = absolutize("top-dir/file-2");
+    Path aDir = absolutize("top-dir/a-dir");
+    Path file3 = absolutize("top-dir/a-dir/file-3");
+    Path file4 = absolutize("file-4");
+
+    topDir.createDirectory();
+    FileSystemUtils.createEmptyFile(file1);
+    FileSystemUtils.createEmptyFile(file2);
+    aDir.createDirectory();
+    FileSystemUtils.createEmptyFile(file3);
+    FileSystemUtils.createEmptyFile(file4);
+
+    Path toDelete = topDir;
+    Path outboundLink = absolutize("top-dir/outbound-link");
+    outboundLink.createSymbolicLink(file4);
+
+    toDelete.deleteTree();
+
+    assertThat(file4.exists()).isTrue();
+    assertThat(topDir.exists()).isFalse();
+    assertThat(file1.exists()).isFalse();
+    assertThat(file2.exists()).isFalse();
+    assertThat(aDir.exists()).isFalse();
+    assertThat(file3.exists()).isFalse();
+  }
+
   // Test the date functions
   @Test
   public void testCreateFileChangesTimeOfDirectory() throws Exception {
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
index cac3e38..2b1d57b 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
@@ -18,7 +18,6 @@
 import static com.google.devtools.build.lib.vfs.FileSystemUtils.commonAncestor;
 import static com.google.devtools.build.lib.vfs.FileSystemUtils.copyFile;
 import static com.google.devtools.build.lib.vfs.FileSystemUtils.copyTool;
-import static com.google.devtools.build.lib.vfs.FileSystemUtils.deleteTree;
 import static com.google.devtools.build.lib.vfs.FileSystemUtils.moveFile;
 import static com.google.devtools.build.lib.vfs.FileSystemUtils.relativePath;
 import static com.google.devtools.build.lib.vfs.FileSystemUtils.removeExtension;
@@ -655,56 +654,6 @@
   }
 
   @Test
-  public void testDeleteTreeCommandDeletesTree() throws IOException {
-    createTestDirectoryTree();
-    Path toDelete = topDir;
-    deleteTree(toDelete);
-
-    assertThat(file4.exists()).isTrue();
-    assertThat(topDir.exists()).isFalse();
-    assertThat(file1.exists()).isFalse();
-    assertThat(file2.exists()).isFalse();
-    assertThat(aDir.exists()).isFalse();
-    assertThat(file3.exists()).isFalse();
-  }
-
-  @Test
-  public void testDeleteTreeCommandsDeletesUnreadableDirectories() throws IOException {
-    createTestDirectoryTree();
-    Path toDelete = topDir;
-
-    try {
-      aDir.setReadable(false);
-    } catch (UnsupportedOperationException e) {
-      // For file systems that do not support setting readable attribute to
-      // false, this test is simply skipped.
-
-      return;
-    }
-
-    deleteTree(toDelete);
-    assertThat(topDir.exists()).isFalse();
-    assertThat(aDir.exists()).isFalse();
-  }
-
-  @Test
-  public void testDeleteTreeCommandDoesNotFollowLinksOut() throws IOException {
-    createTestDirectoryTree();
-    Path toDelete = topDir;
-    Path outboundLink = fileSystem.getPath("/top-dir/outbound-link");
-    outboundLink.createSymbolicLink(file4);
-
-    deleteTree(toDelete);
-
-    assertThat(file4.exists()).isTrue();
-    assertThat(topDir.exists()).isFalse();
-    assertThat(file1.exists()).isFalse();
-    assertThat(file2.exists()).isFalse();
-    assertThat(aDir.exists()).isFalse();
-    assertThat(file3.exists()).isFalse();
-  }
-
-  @Test
   public void testWriteIsoLatin1() throws Exception {
     Path file = fileSystem.getPath("/does/not/exist/yet.txt");
     FileSystemUtils.writeIsoLatin1(file, "Line 1", "Line 2", "Line 3");
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathGetParentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathGetParentTest.java
index 04def1c..7138b4c 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathGetParentTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathGetParentTest.java
@@ -42,7 +42,7 @@
 
   @After
   public final void deleteTestRoot() throws Exception  {
-    FileSystemUtils.deleteTree(testRoot); // (comment out during debugging)
+    testRoot.deleteTree(); // (comment out during debugging)
   }
 
   private Path getParent(String path) {
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java b/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java
index 1f78b18..7c765fa 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/util/FsApparatus.java
@@ -59,7 +59,7 @@
     Path wd = fs.getPath(TMP_DIR);
 
     try {
-      FileSystemUtils.deleteTree(wd);
+      wd.deleteTree();
     } catch (IOException e) {
       throw new AssertionError(e.getMessage());
     }
diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java
index b945024..299eaea 100644
--- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java
+++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java
@@ -234,7 +234,7 @@
         logger.log(INFO, "Preserving work directory {0}.", tempRoot);
       } else {
         try {
-          FileSystemUtils.deleteTree(tempRoot);
+          tempRoot.deleteTree();
         } catch (IOException e) {
           logger.log(
               SEVERE,