Merge pull request #157 from ddunbar/change-to-keyid-result-v2
Change DB API to find constant key IDs.
diff --git a/docs/buildsystem.rst b/docs/buildsystem.rst
index eb82dd8..f5447fb 100644
--- a/docs/buildsystem.rst
+++ b/docs/buildsystem.rst
@@ -689,3 +689,25 @@
FIXME: currently the archive is always recreated entirely, it would be
preferable in future to correctly update/delete/create the archive file
as required.
+
+Stale File Removal Tool
+-----------------------
+
+**Identifier**: *stale-file-removal*
+
+A tool to remove stale files from previous builds.
+
+The build system records the last value of `expectedOutputs` and compares it
+to the current one. Any path that was previously present, but isn't in the
+current string list will be removed from the file system.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 80
+
+ * - Name
+ - Description
+
+ * - expectedOutputs
+ - A string list of paths that are expected to be produced by the given
+ manifest.
diff --git a/include/llbuild/Basic/FileSystem.h b/include/llbuild/Basic/FileSystem.h
index ab47294..055de7d 100644
--- a/include/llbuild/Basic/FileSystem.h
+++ b/include/llbuild/Basic/FileSystem.h
@@ -59,6 +59,13 @@
/// \returns The file contents, on success, or null on error.
virtual std::unique_ptr<llvm::MemoryBuffer>
getFileContents(const std::string& path) = 0;
+
+ /// Remove the file or directory at the given path.
+ ///
+ /// Directory removal is non-recursive, so only empty directories will be removed.
+ ///
+ /// \returns True if the item was removed, false otherwise.
+ virtual bool remove(const std::string& path) = 0;
/// Get the information to represent the state of the given path in the file
/// system.
diff --git a/include/llbuild/Basic/PlatformUtility.h b/include/llbuild/Basic/PlatformUtility.h
index c78d4a9..1fd4851 100644
--- a/include/llbuild/Basic/PlatformUtility.h
+++ b/include/llbuild/Basic/PlatformUtility.h
@@ -38,6 +38,7 @@
int pipe(int ptHandles[2]);
FILE *popen(const char *command, const char *mode);
int read(int fileHandle, void *destinationBuffer, unsigned int maxCharCount);
+int rmdir(const char *path);
int stat(const char *fileName, StatStruct *buf);
int unlink(const char *fileName);
int write(int fileHandle, void *destinationBuffer, unsigned int maxCharCount);
diff --git a/include/llbuild/BuildSystem/BuildValue.h b/include/llbuild/BuildSystem/BuildValue.h
index 47c8129..d96cb04 100644
--- a/include/llbuild/BuildSystem/BuildValue.h
+++ b/include/llbuild/BuildSystem/BuildValue.h
@@ -57,6 +57,9 @@
/// The signature of a directories contents.
DirectoryTreeSignature,
+ /// A value produced by stale file removal.
+ StaleFileRemoval,
+
/// A value produced by a command which succeeded, but whose output was
/// missing.
MissingOutput,
@@ -123,7 +126,7 @@
}
bool kindHasStringList() const {
- return isDirectoryContents();
+ return isDirectoryContents() || isStaleFileRemoval();
}
bool kindHasOutputInfo() const {
@@ -175,6 +178,23 @@
stringValues.size = size;
}
+ BuildValue(Kind kind, ArrayRef<std::string> values) : kind(kind) {
+ // Construct the concatenated data.
+ uint64_t size = 0;
+ for (auto value: values) {
+ size += value.size() + 1;
+ }
+ char *p, *contents = p = new char[size];
+ for (auto value: values) {
+ assert(value.find('\0') == StringRef::npos);
+ memcpy(p, value.data(), value.size());
+ p += value.size();
+ *p++ = '\0';
+ }
+ stringValues.contents = contents;
+ stringValues.size = size;
+ }
+
std::vector<StringRef> getStringListValues() const {
assert(kindHasStringList());
@@ -296,6 +316,9 @@
static BuildValue makeTarget() {
return BuildValue(Kind::Target);
}
+ static BuildValue makeStaleFileRemoval(ArrayRef<std::string> values) {
+ return BuildValue(Kind::StaleFileRemoval, values);
+ }
/// @}
@@ -311,6 +334,7 @@
bool isDirectoryTreeSignature() const {
return kind == Kind::DirectoryTreeSignature;
}
+ bool isStaleFileRemoval() const { return kind == Kind::StaleFileRemoval; }
bool isMissingOutput() const { return kind == Kind::MissingOutput; }
bool isFailedInput() const { return kind == Kind::FailedInput; }
@@ -327,6 +351,11 @@
assert(isDirectoryContents() && "invalid call for value kind");
return getStringListValues();
}
+
+ std::vector<StringRef> getStaleFileList() const {
+ assert(isStaleFileRemoval() && "invalid call for value kind");
+ return getStringListValues();
+ }
uint64_t getDirectoryTreeSignature() const {
assert(isDirectoryTreeSignature() && "invalid call for value kind");
diff --git a/lib/Basic/FileSystem.cpp b/lib/Basic/FileSystem.cpp
index 39dc7fe..7ab3b17 100644
--- a/lib/Basic/FileSystem.cpp
+++ b/lib/Basic/FileSystem.cpp
@@ -62,6 +62,30 @@
}
return std::unique_ptr<llvm::MemoryBuffer>(result->release());
}
+
+ virtual bool remove(const std::string& path) override {
+ // Assume `path` is a regular file.
+ if (sys::unlink(path.c_str()) == 0) {
+ return true;
+ }
+
+ // Error can't be that `path` is actually a directory.
+ if (errno != EPERM) {
+ return false;
+ }
+
+ // Check if `path` is a directory.
+ struct stat statbuf;
+ if (sys::stat(path.c_str(), &statbuf) != 0) {
+ return false;
+ }
+
+ if (S_ISDIR(statbuf.st_mode)) {
+ return sys::rmdir(path.c_str()) == 0;
+ }
+
+ return false;
+ }
virtual FileInfo getFileInfo(const std::string& path) override {
return FileInfo::getInfoForPath(path);
diff --git a/lib/Basic/PlatformUtility.cpp b/lib/Basic/PlatformUtility.cpp
index 0ebd529..60e4f40 100644
--- a/lib/Basic/PlatformUtility.cpp
+++ b/lib/Basic/PlatformUtility.cpp
@@ -88,6 +88,14 @@
#endif
}
+int sys::rmdir(const char *path) {
+#if defined(_WIN32)
+ return ::_rmdir(path);
+#else
+ return ::rmdir(path);
+#endif
+}
+
int sys::stat(const char *fileName, StatStruct *buf) {
#if defined(_WIN32)
return ::_stat(fileName, buf);
diff --git a/lib/BuildSystem/BuildSystem.cpp b/lib/BuildSystem/BuildSystem.cpp
index 8cdeb54..9b4b137 100644
--- a/lib/BuildSystem/BuildSystem.cpp
+++ b/lib/BuildSystem/BuildSystem.cpp
@@ -46,6 +46,7 @@
#include <memory>
#include <mutex>
+#include <unordered_set>
#include <unistd.h>
@@ -143,10 +144,11 @@
/// The internal schema version.
///
/// Version History:
+ /// * 7: Added StaleFileRemoval to BuildValue
/// * 6: Added DirectoryContents to BuildKey
/// * 5: Switch BuildValue to be BinaryCoding based
/// * 4: Pre-history
- static const uint32_t internalSchemaVersion = 6;
+ static const uint32_t internalSchemaVersion = 7;
BuildSystem& buildSystem;
@@ -2297,6 +2299,182 @@
}
};
+#pragma mark - StaleFileRemovalTool implementation
+
+class StaleFileRemovalCommand : public Command {
+ std::string description;
+
+ std::vector<std::string> expectedOutputs;
+ std::vector<std::string> filesToDelete;
+ bool computedFilesToDelete = false;
+
+ BuildValue priorValue;
+ bool hasPriorResult = false;
+
+ virtual void configureDescription(const ConfigureContext&, StringRef value) override {
+ description = value;
+ }
+
+ virtual void getShortDescription(SmallVectorImpl<char> &result) override {
+ llvm::raw_svector_ostream(result) << (description.empty() ? "Stale file removal" : description);
+ }
+
+ virtual void getVerboseDescription(SmallVectorImpl<char> &result) override {
+ computeFilesToDelete();
+
+ getShortDescription(result);
+ llvm::raw_svector_ostream(result) << ", stale files: [";
+ for (auto fileToDelete : filesToDelete) {
+ llvm::raw_svector_ostream(result) << fileToDelete;
+ if (fileToDelete != *(--filesToDelete.end())) {
+ llvm::raw_svector_ostream(result) << ", ";
+ }
+ }
+ llvm::raw_svector_ostream(result) << "]";
+ }
+
+ virtual void configureInputs(const ConfigureContext& ctx,
+ const std::vector<Node*>& value) override {}
+
+ virtual void configureOutputs(const ConfigureContext& ctx,
+ const std::vector<Node*>& value) override {}
+
+ virtual bool configureAttribute(const ConfigureContext& ctx, StringRef name,
+ StringRef value) override {
+ // No supported attributes.
+ ctx.error("unexpected attribute: '" + name + "'");
+ return false;
+ }
+ virtual bool configureAttribute(const ConfigureContext& ctx, StringRef name,
+ ArrayRef<StringRef> values) override {
+ if (name == "expectedOutputs") {
+ expectedOutputs.reserve(values.size());
+ for (auto value : values) {
+ expectedOutputs.emplace_back(value.str());
+ }
+ return true;
+ }
+
+ ctx.error("unexpected attribute: '" + name + "'");
+ return false;
+ }
+ virtual bool configureAttribute(const ConfigureContext& ctx, StringRef name,
+ ArrayRef<std::pair<StringRef, StringRef>> values) override {
+ // No supported attributes.
+ ctx.error("unexpected attribute: '" + name + "'");
+ return false;
+ }
+
+ virtual BuildValue getResultForOutput(Node* node,
+ const BuildValue& value) override {
+ // If the value was a failed command, propagate the failure.
+ if (value.isFailedCommand() || value.isPropagatedFailureCommand() ||
+ value.isCancelledCommand())
+ return BuildValue::makeFailedInput();
+ if (value.isSkippedCommand())
+ return BuildValue::makeSkippedCommand();
+
+ // Otherwise, this was successful, return the value as-is.
+ return BuildValue::fromData(value.toData());;
+ }
+
+ virtual bool isResultValid(BuildSystem& system,
+ const BuildValue& value) override {
+ // Always re-run stale file removal.
+ return false;
+ }
+
+ virtual void start(BuildSystemCommandInterface& bsci,
+ core::Task* task) override {}
+
+ virtual void providePriorValue(BuildSystemCommandInterface&, core::Task*,
+ const BuildValue& value) override {
+ hasPriorResult = true;
+ priorValue = BuildValue::fromData(value.toData());
+ }
+
+ virtual void provideValue(BuildSystemCommandInterface&, core::Task*,
+ uintptr_t inputID,
+ const BuildValue& value) override {
+ assert(0 && "unexpected API call");
+ }
+
+ void computeFilesToDelete() {
+ if (computedFilesToDelete) {
+ return;
+ }
+
+ std::vector<StringRef> priorValueList = priorValue.getStaleFileList();
+ std::unordered_set<std::string> priorNodes(priorValueList.begin(), priorValueList.end());
+ std::unordered_set<std::string> expectedNodes(expectedOutputs.begin(), expectedOutputs.end());
+
+ std::set_difference(priorNodes.begin(), priorNodes.end(),
+ expectedNodes.begin(), expectedNodes.end(),
+ std::back_inserter(filesToDelete));
+
+ computedFilesToDelete = true;
+ }
+
+ virtual BuildValue execute(BuildSystemCommandInterface& bsci,
+ core::Task* task,
+ QueueJobContext* context) override {
+ // Nothing to do if we do not have a prior result.
+ if (!hasPriorResult || !priorValue.isStaleFileRemoval()) {
+ bsci.getDelegate().commandStarted(this);
+ bsci.getDelegate().commandFinished(this);
+ return BuildValue::makeStaleFileRemoval(expectedOutputs);
+ }
+
+ computeFilesToDelete();
+
+ bsci.getDelegate().commandStarted(this);
+
+ for (auto fileToDelete : filesToDelete) {
+ if (!getBuildSystem(bsci.getBuildEngine()).getDelegate().getFileSystem().remove(fileToDelete)) {
+ bsci.getDelegate().error("", {}, (Twine("cannot remove stale file '") + fileToDelete + "': " + strerror(errno)));
+ }
+ }
+
+ bsci.getDelegate().commandFinished(this);
+
+ // Complete with a successful result.
+ return BuildValue::makeStaleFileRemoval(expectedOutputs);
+ }
+
+public:
+ StaleFileRemovalCommand(const StringRef name)
+ : Command(name), priorValue(BuildValue::makeInvalid()) {}
+};
+
+class StaleFileRemovalTool : public Tool {
+public:
+ using Tool::Tool;
+
+ virtual bool configureAttribute(const ConfigureContext& ctx, StringRef name,
+ StringRef value) override {
+ // No supported attributes.
+ ctx.error("unexpected attribute: '" + name + "'");
+ return false;
+ }
+ virtual bool configureAttribute(const ConfigureContext& ctx, StringRef name,
+ ArrayRef<StringRef> values) override {
+ // No supported attributes.
+ ctx.error("unexpected attribute: '" + name + "'");
+ return false;
+ }
+ virtual bool configureAttribute(
+ const ConfigureContext& ctx, StringRef name,
+ ArrayRef<std::pair<StringRef, StringRef>> values) override {
+ // No supported attributes.
+ ctx.error("unexpected attribute: '" + name + "'");
+ return false;
+ }
+
+ virtual std::unique_ptr<Command> createCommand(StringRef name) override {
+ return llvm::make_unique<StaleFileRemovalCommand>(name);
+ }
+};
+
#pragma mark - BuildSystemFileDelegate
BuildSystemDelegate& BuildSystemFileDelegate::getSystemDelegate() {
@@ -2354,6 +2532,8 @@
return llvm::make_unique<SymlinkTool>(name);
} else if (name == "archive") {
return llvm::make_unique<ArchiveTool>(name);
+ } else if (name == "stale-file-removal") {
+ return llvm::make_unique<StaleFileRemovalTool>(name);
}
return nullptr;
diff --git a/lib/BuildSystem/BuildValue.cpp b/lib/BuildSystem/BuildValue.cpp
index 27e1abf..651e47f 100644
--- a/lib/BuildSystem/BuildValue.cpp
+++ b/lib/BuildSystem/BuildValue.cpp
@@ -36,6 +36,7 @@
CASE(CancelledCommand);
CASE(SkippedCommand);
CASE(Target);
+ CASE(StaleFileRemoval);
#undef CASE
}
return "<unknown>";
diff --git a/products/libllbuild/BuildSystem-C-API.cpp b/products/libllbuild/BuildSystem-C-API.cpp
index 2b58f71..c5262b8 100644
--- a/products/libllbuild/BuildSystem-C-API.cpp
+++ b/products/libllbuild/BuildSystem-C-API.cpp
@@ -82,6 +82,14 @@
return result;
}
+ virtual bool remove(const std::string& path) override {
+ if (!cAPIDelegate.fs_remove) {
+ return localFileSystem->remove(path);
+ }
+
+ return cAPIDelegate.fs_remove(cAPIDelegate.context, path.c_str());
+ }
+
virtual basic::FileInfo getFileInfo(const std::string& path) override {
if (!cAPIDelegate.fs_get_file_info) {
return localFileSystem->getFileInfo(path);
diff --git a/products/libllbuild/public-api/llbuild/buildsystem.h b/products/libllbuild/public-api/llbuild/buildsystem.h
index f8c7c50..3935feb 100644
--- a/products/libllbuild/public-api/llbuild/buildsystem.h
+++ b/products/libllbuild/public-api/llbuild/buildsystem.h
@@ -215,6 +215,13 @@
bool (*fs_get_file_contents)(void* context, const char* path,
llb_data_t* data_out);
+ /// Remove file or directory at given path.
+ ///
+ /// Xparam path The path to remove.
+ ///
+ /// \\returns True on success.
+ bool (*fs_remove)(void* context, const char* path);
+
/// Get the file information for the given path.
void (*fs_get_file_info)(void* context, const char* path,
llb_fs_file_info_t* data_out);
diff --git a/tests/BuildSystem/Build/stale-file-removal.llbuild b/tests/BuildSystem/Build/stale-file-removal.llbuild
new file mode 100644
index 0000000..125b1e9
--- /dev/null
+++ b/tests/BuildSystem/Build/stale-file-removal.llbuild
@@ -0,0 +1,57 @@
+# Check the handling of stale file removal.
+#
+# We use 'grep' to slice out two different subfiles from the same file.
+#
+# RUN: rm -rf %t.build
+# RUN: mkdir -p %t.build
+#
+# RUN: grep -A1000 "VERSION-BEGIN-[1]" %s | grep -B10000 "VERSION-END-1" | grep -ve '^--$' > %t.build/build-1.llbuild
+# RUN: %{llbuild} buildsystem build --serial --chdir %t.build -f build-1.llbuild > %t.1.out
+# RUN: %{FileCheck} --check-prefix CHECK-VERSION-1 --input-file=%t.1.out %s
+#
+# CHECK-VERSION-1: STALE-FILE-REMOVAL
+#
+# RUN: grep -A1000 "VERSION-BEGIN-[2]" %s | grep -B10000 "VERSION-END-2" | grep -ve '^--$' > %t.build/build-2.llbuild
+# Create stale file
+# RUN: touch %t.build/a.out
+# RUN: %{llbuild} buildsystem build --serial --chdir %t.build -f build-2.llbuild > %t2.out
+# RUN: %{FileCheck} --check-prefix CHECK-VERSION-2 --input-file %t2.out %s
+# Check for absence of stale file
+# RUN: test ! -f %t.build/a.out
+#
+# CHECK-VERSION-2: STALE-FILE-REMOVAL-2
+#
+
+##### VERSION-BEGIN-1 #####
+
+client:
+ name: basic
+
+targets:
+ "": ["<all>"]
+
+commands:
+ C.1:
+ tool: stale-file-removal
+ description: STALE-FILE-REMOVAL
+ expectedOutputs: ["a.out"]
+ outputs: ["<all>"]
+
+##### VERSION-END-1 #####
+
+##### VERSION-BEGIN-2 #####
+
+client:
+ name: basic
+
+targets:
+ "": ["<all>"]
+
+commands:
+ C.1:
+ tool: stale-file-removal
+ description: STALE-FILE-REMOVAL-2
+ expectedOutputs: ["b.out"]
+ outputs: ["<all>"]
+
+##### VERSION-END-2 #####
diff --git a/unittests/BuildSystem/BuildSystemTaskTests.cpp b/unittests/BuildSystem/BuildSystemTaskTests.cpp
index f907fab..2ecde55 100644
--- a/unittests/BuildSystem/BuildSystemTaskTests.cpp
+++ b/unittests/BuildSystem/BuildSystemTaskTests.cpp
@@ -324,4 +324,87 @@
}), delegate.getMessages());
}
+// Tests the behaviour of StaleFileRemovalTool
+TEST(BuildSystemTaskTests, staleFileRemoval) {
+ 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");
+ MockBuildSystemDelegate delegate(/*trackAllMessages=*/true);
+ BuildSystem system(delegate);
+
+ SmallString<256> builddb{ tempDir.str() };
+ sys::path::append(builddb, "build.db");
+ 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: ["b.out"]
+)END";
+ }
+
+ MockBuildSystemDelegate delegate2(/*trackAllMessages=*/true);
+ BuildSystem system2(delegate2);
+ system2.attachDB(builddb.c_str(), nullptr);
+ loadingResult = system2.loadDescription(manifest);
+ ASSERT_TRUE(loadingResult);
+ result = system2.build(keyToBuild);
+
+ ASSERT_TRUE(result.getValue().isStaleFileRemoval());
+ ASSERT_EQ(result.getValue().getStaleFileList().size(), 1UL);
+ ASSERT_TRUE(strcmp(result.getValue().getStaleFileList()[0].str().c_str(), "b.out") == 0);
+
+ ASSERT_EQ(std::vector<std::string>({
+ "commandPreparing(C.1)",
+ "commandStarted(C.1)",
+ // FIXME: Maybe it's worth creating a virtual FileSystem implementation and checking if `remove` has been called
+ "cannot remove stale file 'a.out': No such file or directory",
+ "commandFinished(C.1)",
+ }), delegate2.getMessages());
+}
+
}
diff --git a/unittests/BuildSystem/BuildValueTest.cpp b/unittests/BuildSystem/BuildValueTest.cpp
index 8fa399a..70ec4e7 100644
--- a/unittests/BuildSystem/BuildValueTest.cpp
+++ b/unittests/BuildSystem/BuildValueTest.cpp
@@ -161,4 +161,25 @@
}
}
+TEST(BuildValueTest, staleFileRemovalValues) {
+ std::vector<std::string> files { "a.out", "Info.plist" };
+
+ {
+ BuildValue a = BuildValue::makeStaleFileRemoval(ArrayRef<std::string>(files));
+ auto nodes = a.getStaleFileList();
+ EXPECT_EQ(2UL, nodes.size());
+ EXPECT_EQ(nodes[0], "a.out");
+ EXPECT_EQ(nodes[1], "Info.plist");
+ }
+
+ {
+ BuildValue a = BuildValue::makeStaleFileRemoval(ArrayRef<std::string>(files));
+ BuildValue b = BuildValue::fromData(a.toData());
+ auto nodes = b.getStaleFileList();
+ EXPECT_EQ(2UL, nodes.size());
+ EXPECT_EQ(nodes[0], "a.out");
+ EXPECT_EQ(nodes[1], "Info.plist");
+ }
+}
+
}