<rdar://problem/30961839> Implement support for stale file removal

- Implement support for recursive removal of directories
- Implement support for specifying root paths outside of which files
  will not be removed
- Add test coverage for new features and a test for verifying that
  symlink targets will not be removed
diff --git a/docs/buildsystem.rst b/docs/buildsystem.rst
index f5447fb..d2b3e82 100644
--- a/docs/buildsystem.rst
+++ b/docs/buildsystem.rst
@@ -711,3 +711,8 @@
    * - expectedOutputs
      - A string list of paths that are expected to be produced by the given
        manifest.
+
+   * - roots
+     - A string lists of paths that are the only allowed root paths for files
+       to be deleted. Files outside of those paths will not be removed by
+       stale file removal.
diff --git a/include/llbuild/Basic/FileSystem.h b/include/llbuild/Basic/FileSystem.h
index 055de7d..7e61f71 100644
--- a/include/llbuild/Basic/FileSystem.h
+++ b/include/llbuild/Basic/FileSystem.h
@@ -62,7 +62,7 @@
 
   /// Remove the file or directory at the given path.
   ///
-  /// Directory removal is non-recursive, so only empty directories will be removed.
+  /// Directory removal is recursive.
   ///
   /// \returns True if the item was removed, false otherwise.
   virtual bool remove(const std::string& path) = 0;
diff --git a/include/llbuild/Basic/PlatformUtility.h b/include/llbuild/Basic/PlatformUtility.h
index 1fd4851..2415007 100644
--- a/include/llbuild/Basic/PlatformUtility.h
+++ b/include/llbuild/Basic/PlatformUtility.h
@@ -40,6 +40,7 @@
 int read(int fileHandle, void *destinationBuffer, unsigned int maxCharCount);
 int rmdir(const char *path);
 int stat(const char *fileName, StatStruct *buf);
+int symlink(const char *source, const char *target);
 int unlink(const char *fileName);
 int write(int fileHandle, void *destinationBuffer, unsigned int maxCharCount);
 }
diff --git a/lib/Basic/FileSystem.cpp b/lib/Basic/FileSystem.cpp
index 7ab3b17..42db497 100644
--- a/lib/Basic/FileSystem.cpp
+++ b/lib/Basic/FileSystem.cpp
@@ -14,12 +14,99 @@
 #include "llbuild/Basic/PlatformUtility.h"
 
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/FileSystem.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/MemoryBuffer.h"
 
 #include <cassert>
 #include <cstring>
 
+// Cribbed from llvm, where it's been since removed.
+namespace {
+  using namespace std;
+  using namespace llvm;
+  using namespace llvm::sys::fs;
+
+  static std::error_code fillStatus(int StatRet, const struct stat &Status,
+                                    file_status &Result) {
+    if (StatRet != 0) {
+      std::error_code ec(errno, std::generic_category());
+      if (ec == errc::no_such_file_or_directory)
+        Result = file_status(file_type::file_not_found);
+      else
+        Result = file_status(file_type::status_error);
+      return ec;
+    }
+
+    file_type Type = file_type::type_unknown;
+
+    if (S_ISDIR(Status.st_mode))
+      Type = file_type::directory_file;
+    else if (S_ISREG(Status.st_mode))
+      Type = file_type::regular_file;
+    else if (S_ISBLK(Status.st_mode))
+      Type = file_type::block_file;
+    else if (S_ISCHR(Status.st_mode))
+      Type = file_type::character_file;
+    else if (S_ISFIFO(Status.st_mode))
+      Type = file_type::fifo_file;
+    else if (S_ISSOCK(Status.st_mode))
+      Type = file_type::socket_file;
+    else if (S_ISLNK(Status.st_mode))
+      Type = file_type::symlink_file;
+
+    perms Perms = static_cast<perms>(Status.st_mode);
+    Result =
+    file_status(Type, Perms, Status.st_dev, Status.st_ino, Status.st_mtime,
+                Status.st_uid, Status.st_gid, Status.st_size);
+
+    return std::error_code();
+  }
+
+  std::error_code link_status(const Twine &Path, file_status &Result) {
+    SmallString<128> PathStorage;
+    StringRef P = Path.toNullTerminatedStringRef(PathStorage);
+
+    struct stat Status;
+    int StatRet = ::lstat(P.begin(), &Status);
+    return fillStatus(StatRet, Status, Result);
+  }
+
+  error_code _remove_all_r(StringRef path, file_type ft, uint32_t &count) {
+    if (ft == file_type::directory_file) {
+      error_code ec;
+      directory_iterator i(path, ec);
+      if (ec)
+        return ec;
+
+      for (directory_iterator e; i != e; i.increment(ec)) {
+        if (ec)
+          return ec;
+
+        file_status st;
+
+        if (error_code ec = link_status(i->path(), st))
+          return ec;
+
+        if (error_code ec = _remove_all_r(i->path(), st.type(), count))
+          return ec;
+      }
+
+      if (error_code ec = remove(path, false))
+        return ec;
+
+      ++count; // Include the directory itself in the items removed.
+    } else {
+      if (error_code ec = remove(path, false))
+        return ec;
+
+      ++count;
+    }
+
+    return error_code();
+  }
+}
+
 using namespace llbuild;
 using namespace llbuild::basic;
 
@@ -46,7 +133,7 @@
 
   virtual bool
   createDirectory(const std::string& path) override {
-    if (!sys::mkdir(path.c_str())) {
+    if (!llbuild::basic::sys::mkdir(path.c_str())) {
       if (errno != EEXIST) {
         return false;
       }
@@ -63,25 +150,34 @@
     return std::unique_ptr<llvm::MemoryBuffer>(result->release());
   }
 
+  bool rm_tree(const char* path) {
+    uint32_t count = 0;
+    return !_remove_all_r(path, file_type::directory_file, count);
+  }
+
   virtual bool remove(const std::string& path) override {
     // Assume `path` is a regular file.
-    if (sys::unlink(path.c_str()) == 0) {
+    if (llbuild::basic::sys::unlink(path.c_str()) == 0) {
       return true;
     }
 
-    // Error can't be that `path` is actually a directory.
-    if (errno != EPERM) {
+    // Error can't be that `path` is actually a directory (on Linux `EISDIR` will be returned since 2.1.132).
+    if (errno != EPERM && errno != EISDIR) {
       return false;
     }
 
     // Check if `path` is a directory.
     struct stat statbuf;
-    if (sys::stat(path.c_str(), &statbuf) != 0) {
+    if (llbuild::basic::sys::lstat(path.c_str(), &statbuf) != 0) {
       return false;
     }
 
     if (S_ISDIR(statbuf.st_mode)) {
-      return sys::rmdir(path.c_str()) == 0;
+      if (llbuild::basic::sys::rmdir(path.c_str()) == 0) {
+        return true;
+      } else {
+        return rm_tree(path.c_str());
+      }
     }
 
     return false;
diff --git a/lib/Basic/PlatformUtility.cpp b/lib/Basic/PlatformUtility.cpp
index 60e4f40..fba346f 100644
--- a/lib/Basic/PlatformUtility.cpp
+++ b/lib/Basic/PlatformUtility.cpp
@@ -104,6 +104,14 @@
 #endif
 }
 
+int sys::symlink(const char *source, const char *target) {
+#if defined(_WIN32)
+  return ::_symlink(source, target);
+#else
+  return ::symlink(source, target);
+#endif
+}
+
 int sys::unlink(const char *fileName) {
 #if defined(_WIN32)
   return ::_unlink(fileName);
diff --git a/lib/BuildSystem/BuildSystem.cpp b/lib/BuildSystem/BuildSystem.cpp
index 9b4b137..802ce18 100644
--- a/lib/BuildSystem/BuildSystem.cpp
+++ b/lib/BuildSystem/BuildSystem.cpp
@@ -2306,11 +2306,14 @@
 
   std::vector<std::string> expectedOutputs;
   std::vector<std::string> filesToDelete;
+  std::vector<std::string> roots;
   bool computedFilesToDelete = false;
 
   BuildValue priorValue;
   bool hasPriorResult = false;
 
+  char path_separator = llvm::sys::path::get_separator()[0];
+
   virtual void configureDescription(const ConfigureContext&, StringRef value) override {
     description = value;
   }
@@ -2353,6 +2356,12 @@
         expectedOutputs.emplace_back(value.str());
       }
       return true;
+    } else if (name == "roots") {
+      roots.reserve(values.size());
+      for (auto value : values) {
+        roots.emplace_back(value.str());
+      }
+      return true;
     }
 
     ctx.error("unexpected attribute: '" + name + "'");
@@ -2430,6 +2439,28 @@
     bsci.getDelegate().commandStarted(this);
 
     for (auto fileToDelete : filesToDelete) {
+      // If no root paths are specified, any path is valid.
+      bool isLocatedUnderRootPath = roots.size() == 0 ? true : false;
+
+      // If root paths are defined, stale file paths should be absolute.
+      if (roots.size() > 0 && fileToDelete[0] != path_separator) {
+        bsci.getDelegate().error("", {}, (Twine("Stale file '") + fileToDelete + "' has a relative path. This is invalid in combination with the root path attribute."));
+        continue;
+      }
+
+      // Check if the file is located under one of the allowed root paths.
+      for (auto root : roots) {
+        auto res = std::mismatch(root.begin(), root.end(), fileToDelete.begin());
+        if (res.first == root.end() && ((*(res.first++) == '\0') || (*(res.first++) == path_separator))) {
+          isLocatedUnderRootPath = true;
+        }
+      }
+
+      if (!isLocatedUnderRootPath) {
+        bsci.getDelegate().error("", {}, (Twine("Stale file '") + fileToDelete + "' is located outside of the allowed root paths."));
+        continue;
+      }
+
       if (!getBuildSystem(bsci.getBuildEngine()).getDelegate().getFileSystem().remove(fileToDelete)) {
         bsci.getDelegate().error("", {}, (Twine("cannot remove stale file '") + fileToDelete + "': " + strerror(errno)));
       }
diff --git a/llbuild.xcodeproj/project.pbxproj b/llbuild.xcodeproj/project.pbxproj
index 64c1cea..bb99339 100644
--- a/llbuild.xcodeproj/project.pbxproj
+++ b/llbuild.xcodeproj/project.pbxproj
@@ -66,6 +66,7 @@
 /* Begin PBXBuildFile section */
 		9D0A6D811E1FFEA800BE636F /* TempDir.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9D0A6D7F1E1FFEA800BE636F /* TempDir.cpp */; };
 		9D2107C61DFADDFA00BE26FF /* libcurses.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E15B6EC61B546A2C00643066 /* libcurses.dylib */; };
+		9D5A5C311EC5FAE600DC84CC /* TempDir.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9D0A6D7F1E1FFEA800BE636F /* TempDir.cpp */; };
 		9DADBBAD1E256C73005B4869 /* PlatformUtility.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9DADBBAC1E256C52005B4869 /* PlatformUtility.cpp */; };
 		9DB047BA1DF9D4A4006CDF52 /* libgtest_main.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E1A224E619F99C580059043E /* libgtest_main.a */; };
 		9DB047BB1DF9D4A4006CDF52 /* libgtest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E1A224DD19F99B0E0059043E /* libgtest.a */; };
@@ -2674,6 +2675,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				9D5A5C311EC5FAE600DC84CC /* TempDir.cpp in Sources */,
 				E13812A21C53708E000092C0 /* FileSystemTest.cpp in Sources */,
 				E120B9EE1E4E65EB00B28469 /* ShellUtilityTest.cpp in Sources */,
 				E147DF1A1BA81D5A0032D08E /* SerialQueueTest.cpp in Sources */,
diff --git a/unittests/Basic/CMakeLists.txt b/unittests/Basic/CMakeLists.txt
index 6a65394..c1df379 100644
--- a/unittests/Basic/CMakeLists.txt
+++ b/unittests/Basic/CMakeLists.txt
@@ -3,6 +3,7 @@
   FileSystemTest.cpp
   SerialQueueTest.cpp
   ShellUtilityTest.cpp
+  ../BuildSystem/TempDir.cpp
   )
 
 target_link_libraries(BasicTests llbuildBasic llvmSupport)
diff --git a/unittests/Basic/FileSystemTest.cpp b/unittests/Basic/FileSystemTest.cpp
index 66ef337..65182b6 100644
--- a/unittests/Basic/FileSystemTest.cpp
+++ b/unittests/Basic/FileSystemTest.cpp
@@ -10,11 +10,15 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "../BuildSystem/TempDir.h"
+
 #include "llbuild/Basic/FileSystem.h"
 #include "llbuild/Basic/LLVM.h"
+#include "llbuild/Basic/PlatformUtility.h"
 
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
 #include "llvm/Support/raw_ostream.h"
 
 #include "gtest/gtest.h"
@@ -56,4 +60,97 @@
   EXPECT_FALSE(ec);
 }
 
+TEST(FileSystemTest, testRecursiveRemoval) {
+  TmpDir rootTempDir{ __FUNCTION__ };
+
+  SmallString<256> tempDir { rootTempDir.str() };
+  llvm::sys::path::append(tempDir, "root");
+  sys::mkdir(tempDir.c_str());
+
+  SmallString<256> file{ tempDir.str() };
+  llvm::sys::path::append(file, "test.txt");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(file.str(), ec, llvm::sys::fs::F_Text);
+    EXPECT_FALSE(ec);
+    os << "Hello, world!\n";
+    os.close();
+  }
+
+  SmallString<256> dir{ tempDir.str() };
+  llvm::sys::path::append(dir, "subdir");
+  sys::mkdir(dir.c_str());
+
+  llvm::sys::path::append(dir, "file_in_subdir.txt");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(dir.str(), ec, llvm::sys::fs::F_Text);
+    EXPECT_FALSE(ec);
+    os << "Hello, world!\n";
+    os.close();
+  }
+
+  auto fs = createLocalFileSystem();
+  bool result = fs->remove(tempDir.c_str());
+  EXPECT_TRUE(result);
+
+  struct stat statbuf;
+  EXPECT_EQ(-1, sys::stat(tempDir.c_str(), &statbuf));
+  EXPECT_EQ(ENOENT, errno);
+}
+
+TEST(FileSystemTest, testRecursiveRemovalDoesNotFollowSymlinks) {
+  TmpDir rootTempDir{ __FUNCTION__ };
+
+  SmallString<256> file{ rootTempDir.str() };
+  llvm::sys::path::append(file, "test.txt");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(file.str(), ec, llvm::sys::fs::F_Text);
+    EXPECT_FALSE(ec);
+    os << "Hello, world!\n";
+    os.close();
+  }
+
+  SmallString<256> otherDir{ rootTempDir.str() };
+  llvm::sys::path::append(otherDir, "other_dir");
+  sys::mkdir(otherDir.c_str());
+
+  SmallString<256> otherFile{ otherDir.str() };
+  llvm::sys::path::append(otherFile, "test.txt");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(otherFile.str(), ec, llvm::sys::fs::F_Text);
+    EXPECT_FALSE(ec);
+    os << "Hello, world!\n";
+    os.close();
+  }
+
+  SmallString<256> tempDir { rootTempDir.str() };
+  llvm::sys::path::append(tempDir, "root");
+  sys::mkdir(tempDir.c_str());
+
+  SmallString<256> linkPath { tempDir.str() };
+  llvm::sys::path::append(linkPath, "link.txt");
+  int res = sys::symlink(file.c_str(), linkPath.c_str());
+  EXPECT_EQ(res, 0);
+
+  SmallString<256> directoryLinkPath { tempDir.str() };
+  llvm::sys::path::append(directoryLinkPath, "link_to_other_dir");
+  res = sys::symlink(otherDir.c_str(), directoryLinkPath.c_str());
+  EXPECT_EQ(res, 0);
+
+  auto fs = createLocalFileSystem();
+  bool result = fs->remove(tempDir.c_str());
+  EXPECT_TRUE(result);
+
+  struct stat statbuf;
+  EXPECT_EQ(-1, sys::stat(tempDir.c_str(), &statbuf));
+  EXPECT_EQ(ENOENT, errno);
+  // Verify that the symlink target still exists.
+  EXPECT_EQ(0, sys::stat(file.c_str(), &statbuf));
+  // Verify that we did not delete the symlinked directories contents.
+  EXPECT_EQ(0, sys::stat(otherFile.c_str(), &statbuf));
+}
+
 }
diff --git a/unittests/BuildSystem/BuildSystemTaskTests.cpp b/unittests/BuildSystem/BuildSystemTaskTests.cpp
index ffee42c..f8fd3b6 100644
--- a/unittests/BuildSystem/BuildSystemTaskTests.cpp
+++ b/unittests/BuildSystem/BuildSystemTaskTests.cpp
@@ -410,4 +410,264 @@
   }), delegate.getMessages());
 }
 
+TEST(BuildSystemTaskTests, staleFileRemovalWithRoots) {
+  TmpDir tempDir{ __FUNCTION__ };
+
+  SmallString<256> manifest{ tempDir.str() };
+  sys::path::append(manifest, "manifest.llbuild");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      description: STALE-FILE-REMOVAL
+      expectedOutputs: ["/bar/a.out"]
+)END";
+  }
+
+  auto keyToBuild = BuildKey::makeCommand("C.1");
+
+  SmallString<256> builddb{ tempDir.str() };
+  sys::path::append(builddb, "build.db");
+
+  {
+    MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+    BuildSystem system(delegate);
+    system.attachDB(builddb.c_str(), nullptr);
+
+    bool loadingResult = system.loadDescription(manifest);
+    ASSERT_TRUE(loadingResult);
+
+    auto result = system.build(keyToBuild);
+
+    ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+    ASSERT_EQ(result.getValue().getStaleFileList().size(), 1UL);
+    ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "/bar/a.out") == 0);
+
+    ASSERT_EQ(std::vector<std::string>({
+      "commandPreparing(C.1)",
+      "commandStarted(C.1)",
+      "commandFinished(C.1)",
+    }), delegate.getMessages());
+  }
+
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      description: STALE-FILE-REMOVAL
+      expectedOutputs: ["/bar/b.out", "/foo", "/foobar.txt"]
+      roots: ["/foo"]
+)END";
+  }
+
+  MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+  BuildSystem system(delegate);
+  system.attachDB(builddb.c_str(), nullptr);
+  bool loadingResult = system.loadDescription(manifest);
+  ASSERT_TRUE(loadingResult);
+  auto result = system.build(keyToBuild);
+
+  ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+  ASSERT_EQ(result.getValue().getStaleFileList().size(), 3UL);
+  ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "/bar/b.out") == 0);
+  ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[1].str().c_str(), "/foo") == 0);
+  ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[2].str().c_str(), "/foobar.txt") == 0);
+
+  ASSERT_EQ(std::vector<std::string>({
+    "commandPreparing(C.1)",
+    "commandStarted(C.1)",
+    "Stale file '/bar/a.out' is located outside of the allowed root paths.",
+    // FIXME: Enable once stale file removal issues are no longer errros.
+    //"Stale file '/foobar.txt' is located outside of the allowed root paths.",
+    "commandFinished(C.1)",
+  }), delegate.getMessages());
+}
+
+TEST(BuildSystemTaskTests, staleFileRemovalWithRootsEnforcesAbsolutePaths) {
+  TmpDir tempDir{ __FUNCTION__ };
+
+  SmallString<256> manifest{ tempDir.str() };
+  sys::path::append(manifest, "manifest.llbuild");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      description: STALE-FILE-REMOVAL
+      expectedOutputs: ["a.out"]
+)END";
+  }
+
+  auto keyToBuild = BuildKey::makeCommand("C.1");
+
+  SmallString<256> builddb{ tempDir.str() };
+  sys::path::append(builddb, "build.db");
+
+  {
+    MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+    BuildSystem system(delegate);
+    system.attachDB(builddb.c_str(), nullptr);
+
+    bool loadingResult = system.loadDescription(manifest);
+    ASSERT_TRUE(loadingResult);
+
+    auto result = system.build(keyToBuild);
+
+    ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+    ASSERT_EQ(result.getValue().getStaleFileList().size(), 1UL);
+    ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "a.out") == 0);
+
+    ASSERT_EQ(std::vector<std::string>({
+      "commandPreparing(C.1)",
+      "commandStarted(C.1)",
+      "commandFinished(C.1)",
+    }), delegate.getMessages());
+  }
+
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      description: STALE-FILE-REMOVAL
+      expectedOutputs: ["/bar/b.out"]
+      roots: ["/foo"]
+)END";
+  }
+
+  MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+  BuildSystem system(delegate);
+  system.attachDB(builddb.c_str(), nullptr);
+  bool loadingResult = system.loadDescription(manifest);
+  ASSERT_TRUE(loadingResult);
+  auto result = system.build(keyToBuild);
+
+  ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+  ASSERT_EQ(result.getValue().getStaleFileList().size(), 1UL);
+  ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "/bar/b.out") == 0);
+
+  ASSERT_EQ(std::vector<std::string>({
+    "commandPreparing(C.1)",
+    "commandStarted(C.1)",
+    "Stale file 'a.out' has a relative path. This is invalid in combination with the root path attribute.",
+    "commandFinished(C.1)",
+  }), delegate.getMessages());
+}
+
+TEST(BuildSystemTaskTests, staleFileRemovalWithRootsIsAgnosticToTrailingPathSeparator) {
+  TmpDir tempDir{ __FUNCTION__ };
+
+  SmallString<256> manifest{ tempDir.str() };
+  sys::path::append(manifest, "manifest.llbuild");
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      description: STALE-FILE-REMOVAL
+      expectedOutputs: ["a.out", "/foo/"]
+)END";
+  }
+
+  auto keyToBuild = BuildKey::makeCommand("C.1");
+
+  SmallString<256> builddb{ tempDir.str() };
+  sys::path::append(builddb, "build.db");
+
+  {
+    MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+    BuildSystem system(delegate);
+    system.attachDB(builddb.c_str(), nullptr);
+
+    bool loadingResult = system.loadDescription(manifest);
+    ASSERT_TRUE(loadingResult);
+
+    auto result = system.build(keyToBuild);
+
+    ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+    ASSERT_EQ(result.getValue().getStaleFileList().size(), 2UL);
+    ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "a.out") == 0);
+
+    ASSERT_EQ(std::vector<std::string>({
+      "commandPreparing(C.1)",
+      "commandStarted(C.1)",
+      "commandFinished(C.1)",
+    }), delegate.getMessages());
+  }
+
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream os(manifest, ec, llvm::sys::fs::F_Text);
+    assert(!ec);
+
+    os << R"END(
+client:
+  name: mock
+
+commands:
+    C.1:
+      tool: stale-file-removal
+      description: STALE-FILE-REMOVAL
+      expectedOutputs: ["/bar/b.out"]
+      roots: ["/foo/"]
+)END";
+  }
+
+  MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+  BuildSystem system(delegate);
+  system.attachDB(builddb.c_str(), nullptr);
+  bool loadingResult = system.loadDescription(manifest);
+  ASSERT_TRUE(loadingResult);
+  auto result = system.build(keyToBuild);
+
+  ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+  ASSERT_EQ(result.getValue().getStaleFileList().size(), 1UL);
+  ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "/bar/b.out") == 0);
+
+  ASSERT_EQ(std::vector<std::string>({
+    "commandPreparing(C.1)",
+    "commandStarted(C.1)",
+    "cannot remove stale file '/foo/': No such file or directory",
+    "Stale file 'a.out' has a relative path. This is invalid in combination with the root path attribute.",
+    "commandFinished(C.1)",
+  }), delegate.getMessages());
+}
+
 }
diff --git a/unittests/BuildSystem/TempDir.cpp b/unittests/BuildSystem/TempDir.cpp
index 10fa8da..ed01c56 100644
--- a/unittests/BuildSystem/TempDir.cpp
+++ b/unittests/BuildSystem/TempDir.cpp
@@ -12,70 +12,12 @@
 
 #include "TempDir.h"
 
+#include "llbuild/Basic/FileSystem.h"
+
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/SourceMgr.h"
 
-// TODO Move this into some kind of libtestSupport?
-// Cribbed from llvm, where it's been since removed.
-namespace {
-
-    using namespace std;
-    using namespace llvm;
-    using namespace llvm::sys::fs;
-
-    error_code _remove_all_r(StringRef path, file_type ft, uint32_t &count) {
-        if (ft == file_type::directory_file) {
-            error_code ec;
-            directory_iterator i(path, ec);
-            if (ec)
-                return ec;
-
-            for (directory_iterator e; i != e; i.increment(ec)) {
-                if (ec)
-                    return ec;
-
-                file_status st;
-
-                if (error_code ec = i->status(st))
-                    return ec;
-
-                if (error_code ec = _remove_all_r(i->path(), st.type(), count))
-                    return ec;
-            }
-
-            if (error_code ec = remove(path, false))
-                return ec;
-
-            ++count; // Include the directory itself in the items removed.
-        } else {
-            if (error_code ec = remove(path, false))
-                return ec;
-
-            ++count;
-        }
-
-        return error_code();
-    }
-
-    error_code remove_all(const Twine &path, uint32_t &num_removed) {
-        SmallString<128> path_storage;
-        StringRef p = path.toStringRef(path_storage);
-        
-        file_status fs;
-        if (error_code ec = status(path, fs))
-            return ec;
-        num_removed = 0;
-        return _remove_all_r(p, fs.type(), num_removed);
-    }
-    
-    error_code remove_all(const Twine &path) {
-        uint32_t num_removed = 0;
-        return remove_all(path, num_removed);
-    }
-    
-}
-
 llbuild::TmpDir::TmpDir(llvm::StringRef namePrefix) {
     llvm::SmallString<256> tempDirPrefix;
     llvm::sys::path::system_temp_directory(true, tempDirPrefix);
@@ -88,9 +30,10 @@
 }
 
 llbuild::TmpDir::~TmpDir() {
-    std::error_code ec = remove_all(llvm::Twine(tempDir));
-    assert(!ec);
-    (void)ec;
+    auto fs = basic::createLocalFileSystem();
+    bool result = fs->remove(tempDir.c_str());
+    assert(result);
+    (void)result;
 }
 
 const char *llbuild::TmpDir::c_str() { return tempDir.c_str(); }