Merge pull request #73 from ddunbar/command-triggers

[BuildSystem] Add support for "command timestamp" nodes.
diff --git a/docs/buildsystem.rst b/docs/buildsystem.rst
index c48fd5f..8ceace8 100644
--- a/docs/buildsystem.rst
+++ b/docs/buildsystem.rst
@@ -275,6 +275,19 @@
        to files in the file system matching the name. This attribute can be used
        to override that default.
 
+   * - is-command-timestamp
+     - A boolean value, indicating whether the node should be used to represent
+       the "timestamp" at which a command was run. When set, the node should
+       also be the output of some command in the graph. Whenever that command is
+       run, the node will take on a value representing the timestamp at which
+       the command was run.
+
+       This node can then be used as a (virtual) input to another command in
+       order to cause the downstream command to rerun whenever the producing
+       command is run.
+
+       Such nodes are always virtual nodes.
+
 .. note::
   FIXME: At some point, we probably want to support custom node types.
 
diff --git a/include/llbuild/BuildSystem/BuildNode.h b/include/llbuild/BuildSystem/BuildNode.h
index 3d1039f..5a8e64d 100644
--- a/include/llbuild/BuildSystem/BuildNode.h
+++ b/include/llbuild/BuildSystem/BuildNode.h
@@ -33,12 +33,20 @@
   /// Whether or not this node is "virtual" (i.e., not a filesystem path).
   bool virtualNode;
 
+  /// Whether this node represents a "command timestamp".
+  ///
+  /// Such nodes should always also be virtual.
+  bool commandTimestamp;
+
 public:
-  explicit BuildNode(StringRef name, bool isVirtual)
-      : Node(name), virtualNode(isVirtual) {}
+  explicit BuildNode(StringRef name, bool isVirtual, bool isCommandTimestamp)
+      : Node(name), virtualNode(isVirtual),
+        commandTimestamp(isCommandTimestamp) {}
 
   bool isVirtual() const { return virtualNode; }
 
+  bool isCommandTimestamp() const { return commandTimestamp; }
+
   virtual bool configureAttribute(const ConfigureContext& ctx, StringRef name,
                                   StringRef value) override;
   virtual bool configureAttribute(const ConfigureContext& ctx, StringRef name,
diff --git a/include/llbuild/Core/BuildEngine.h b/include/llbuild/Core/BuildEngine.h
index 6374e10..959ea5a 100644
--- a/include/llbuild/Core/BuildEngine.h
+++ b/include/llbuild/Core/BuildEngine.h
@@ -30,6 +30,10 @@
 class BuildDB;
 class BuildEngine;
 
+/// A monotonically increasing timestamp identifying which iteration of a build
+/// an event occurred during.
+typedef uint64_t Timestamp;
+
 /// This object contains the result of executing a task to produce the value for
 /// a key.
 struct Result {
@@ -234,6 +238,12 @@
   /// Return the delegate the engine was configured with.
   BuildEngineDelegate* getDelegate();
 
+  /// Get the current build timestamp used by the engine.
+  ///
+  /// The timestamp is a monotonically increasing value which is incremented
+  /// with each requested build.
+  Timestamp getCurrentTimestamp();
+
   /// @name Rule Definition
   /// @{
 
diff --git a/lib/BuildSystem/BuildNode.cpp b/lib/BuildSystem/BuildNode.cpp
index 6e1389f..f0e5b92 100644
--- a/lib/BuildSystem/BuildNode.cpp
+++ b/lib/BuildSystem/BuildNode.cpp
@@ -30,6 +30,19 @@
       virtualNode = true;
     } else if (value == "false") {
       virtualNode = false;
+      commandTimestamp = false;
+    } else {
+      ctx.error("invalid value: '" + value + "' for attribute '"
+                + name + "'");
+      return false;
+    }
+    return true;
+  } else if (name == "is-command-timestamp") {
+    if (value == "true") {
+      commandTimestamp = true;
+      virtualNode = true;
+    } else if (value == "false") {
+      commandTimestamp = false;
     } else {
       ctx.error("invalid value: '" + value + "' for attribute '"
                 + name + "'");
diff --git a/lib/BuildSystem/BuildSystem.cpp b/lib/BuildSystem/BuildSystem.cpp
index 154d64e..8c3feb3 100644
--- a/lib/BuildSystem/BuildSystem.cpp
+++ b/lib/BuildSystem/BuildSystem.cpp
@@ -831,7 +831,8 @@
 std::unique_ptr<BuildNode>
 BuildSystemImpl::lookupNode(StringRef name, bool isImplicit) {
   bool isVirtual = !name.empty() && name[0] == '<' && name.back() == '>';
-  return llvm::make_unique<BuildNode>(name, isVirtual);
+  return llvm::make_unique<BuildNode>(name, isVirtual,
+                                      /*isCommandTimestamp=*/false);
 }
 
 bool BuildSystemImpl::build(StringRef target) {
diff --git a/lib/BuildSystem/ExternalCommand.cpp b/lib/BuildSystem/ExternalCommand.cpp
index 2030e64..8cedaa9 100644
--- a/lib/BuildSystem/ExternalCommand.cpp
+++ b/lib/BuildSystem/ExternalCommand.cpp
@@ -129,7 +129,11 @@
   assert(value.isSuccessfulCommand());
 
   // If the node is virtual, the output is always a virtual input value.
-  if (static_cast<BuildNode*>(node)->isVirtual()) {
+  //
+  // FIXME: Eliminate this, and make the build value array contain an array of
+  // build values.
+  auto buildNode = static_cast<BuildNode*>(node);
+  if (buildNode->isVirtual() && !buildNode->isCommandTimestamp()) {
     return BuildValue::makeVirtualInput();
   }
     
@@ -310,7 +314,14 @@
   // FIXME: We need to delegate to the node here.
   SmallVector<FileInfo, 8> outputInfos;
   for (auto* node: outputs) {
-    if (node->isVirtual()) {
+    if (node->isCommandTimestamp()) {
+      // FIXME: We currently have to shoehorn the timestamp into a fake file
+      // info, but need to refactor the command result to just store the node
+      // subvalues instead.
+      FileInfo info{};
+      info.size = bsci.getBuildEngine().getCurrentTimestamp();
+      outputInfos.push_back(info);
+    } else if (node->isVirtual()) {
       outputInfos.push_back(FileInfo{});
     } else {
       outputInfos.push_back(node->getFileInfo(
diff --git a/lib/Core/BuildEngine.cpp b/lib/Core/BuildEngine.cpp
index 2232deb..d4b5c1d 100644
--- a/lib/Core/BuildEngine.cpp
+++ b/lib/Core/BuildEngine.cpp
@@ -1049,6 +1049,10 @@
     return &delegate;
   }
 
+  Timestamp getCurrentTimestamp() {
+    return currentTimestamp;
+  }
+
   RuleInfo& getRuleInfoForKey(const KeyType& key) {
     // Check if we have already found the rule.
     auto it = ruleInfos.find(key);
@@ -1344,6 +1348,10 @@
   return static_cast<BuildEngineImpl*>(impl)->getDelegate();
 }
 
+Timestamp BuildEngine::getCurrentTimestamp() {
+  return static_cast<BuildEngineImpl*>(impl)->getCurrentTimestamp();
+}
+
 void BuildEngine::addRule(Rule&& rule) {
   static_cast<BuildEngineImpl*>(impl)->addRule(std::move(rule));
 }
diff --git a/tests/BuildSystem/Build/command-dependencies.llbuild b/tests/BuildSystem/Build/command-dependencies.llbuild
new file mode 100644
index 0000000..5c4a9e9
--- /dev/null
+++ b/tests/BuildSystem/Build/command-dependencies.llbuild
@@ -0,0 +1,59 @@
+# Check the behavior of command dependencies.
+#
+# RUN: rm -rf %t.build
+# RUN: mkdir -p %t.build
+# RUN: touch %t.build/input
+# RUN: cp %s %t.build/build.llbuild
+
+# Both commands should run on the initial build.
+#
+# RUN: %{llbuild} buildsystem build --serial --chdir %t.build > %t.initial.out
+# RUN: %{FileCheck} --check-prefix CHECK-INITIAL --input-file %t.initial.out %s
+# CHECK-INITIAL: C.output-2
+# CHECK-INITIAL: C.output-1
+# RUN: diff %t.build/output-1 %t.build/output-2
+
+# No commands should run on a null rebuild.
+#
+# RUN: %{llbuild} buildsystem build --serial --chdir %t.build > %t.null.out
+# RUN: echo EOF >> %t.null.out
+# RUN: %{FileCheck} --check-prefix CHECK-NULL --input-file %t.null.out %s
+# RUN: diff %t.build/output-1 %t.build/output-2
+# CHECK-NULL-NOT: C.output
+# CHECK-NULL: EOF
+
+# Forcing the initial command to run should cause them both to run again.
+#
+# RUN: echo modified >> %t.build/input
+# RUN: %{llbuild} buildsystem build --serial --chdir %t.build > %t.rebuild.out
+# RUN: cat %t.rebuild.out
+# RUN: %{FileCheck} --check-prefix CHECK-REBUILD --input-file %t.rebuild.out %s
+# RUN: diff %t.build/output-1 %t.build/output-2
+#
+# CHECK-REBUILD: C.output-2
+# CHECK-REBUILD: C.output-1
+
+client:
+  name: basic
+
+targets:
+  "": ["<output>"]
+
+nodes:
+  "<C.output-2.timestamp>":
+    is-command-timestamp: true
+
+commands:
+  C.output-1:
+    tool: shell
+    inputs: ["<C.output-2.timestamp>"]
+    outputs: ["<output>"]
+    description: C.output-1
+    args: cp output-2 output-1
+
+  C.output-2:
+    tool: shell
+    inputs: ["input"]
+    outputs: ["<C.output-2.timestamp>"]
+    description: C.output-2
+    args: cp input output-2