Merge pull request #106 from ddunbar/buildsystem-env

[BuildSystem] Add support for a base environment.
diff --git a/docs/buildsystem.rst b/docs/buildsystem.rst
index c292e76..38b3a03 100644
--- a/docs/buildsystem.rst
+++ b/docs/buildsystem.rst
@@ -390,9 +390,13 @@
 
    * - env
      - A mapping of keys and values defining the environment to pass to the
-       launched process. If provided, **only** the entries in this mapping will
-       be exposed in the process environment. If not provided, the process will
-       inherit the environment from the client.
+       launched process. See also `inherit-env`.
+
+   * - inherit-env
+     - A boolean flag controlling whether this command should inherit the base
+       environment provided when executing the build system (either from the
+       command line, or via the internal C APIs), or whether it should only
+       include the entries explicitly provided in the `env` mapping above.
 
    * - allow-missing-inputs
      - A boolean value, indicating whether the commands should be allowed to run
diff --git a/include/llbuild/BuildSystem/BuildExecutionQueue.h b/include/llbuild/BuildSystem/BuildExecutionQueue.h
index b969137..475d326 100644
--- a/include/llbuild/BuildSystem/BuildExecutionQueue.h
+++ b/include/llbuild/BuildSystem/BuildExecutionQueue.h
@@ -108,8 +108,11 @@
   ///
   /// \param commandLine The command line to execute.
   ///
-  /// \param environment The environment to launch with. If empty, the process
-  /// environment will be inherited.
+  /// \param environment The environment to launch with.
+  ///
+  /// \param inheritEnvironment If true, the supplied environment will be
+  /// overlayed on top base environment supplied when creating the queue. If
+  /// false, only the supplied environment will be passed to the subprocess.
   ///
   /// \returns True on success.
   //
@@ -118,7 +121,8 @@
   virtual bool
   executeProcess(QueueJobContext* context,
                  ArrayRef<StringRef> commandLine,
-                 ArrayRef<std::pair<StringRef, StringRef>> environment) = 0;
+                 ArrayRef<std::pair<StringRef, StringRef>> environment,
+                 bool inheritEnvironment = true) = 0;
 
   /// @}
 
@@ -223,8 +227,8 @@
   /// become invalid as soon as the client returns from this API call.
   ///
   /// \param result - Whether the process suceeded, failed or was cancelled.
-  /// \param exitStatus - The raw exit status of the process, or -1 if an error was
-  /// encountered.
+  /// \param exitStatus - The raw exit status of the process, or -1 if an error
+  /// was encountered.
   //
   // FIXME: Need to include additional information on the status here, e.g., the
   // signal status, and the process output (if buffering).
@@ -236,7 +240,8 @@
 /// Create an execution queue that schedules jobs to individual lanes with a
 /// capped limit on the number of concurrent lanes.
 BuildExecutionQueue *createLaneBasedExecutionQueue(
-    BuildExecutionQueueDelegate& delegate, int numLanes);
+    BuildExecutionQueueDelegate& delegate, int numLanes,
+    const char* const* environment);
 
 }
 }
diff --git a/include/llbuild/BuildSystem/BuildSystemFrontend.h b/include/llbuild/BuildSystem/BuildSystemFrontend.h
index 8c90b23..b70fdfa 100644
--- a/include/llbuild/BuildSystem/BuildSystemFrontend.h
+++ b/include/llbuild/BuildSystem/BuildSystemFrontend.h
@@ -295,6 +295,14 @@
   /// The path of the build trace output file to use, if any.
   std::string traceFilePath = "";
 
+  /// The base environment to use when executing subprocesses.
+  ///
+  /// The format is expected to match that of `::main()`, i.e. a null-terminated
+  /// array of pointers to null terminated C strings.
+  ///
+  /// If empty, the environment of the calling process will be used.
+  const char* const* environment = nullptr;
+
   /// The positional arguments.
   std::vector<std::string> positionalArgs;
 
diff --git a/lib/BuildSystem/BuildSystem.cpp b/lib/BuildSystem/BuildSystem.cpp
index a470c0b..3aaa600 100644
--- a/lib/BuildSystem/BuildSystem.cpp
+++ b/lib/BuildSystem/BuildSystem.cpp
@@ -959,7 +959,10 @@
 
   /// The style of dependencies used.
   DepsStyle depsStyle = DepsStyle::Unused;
-  
+
+  /// Whether to inherit the base environment.
+  bool inheritEnv = true;
+
   virtual uint64_t getSignature() override {
     // FIXME: Use a more appropriate hashing infrastructure.
     using llvm::hash_combine;
@@ -975,6 +978,7 @@
       code = hash_combine(code, path);
     }
     code = hash_combine(code, int(depsStyle));
+    code = hash_combine(code, int(inheritEnv));
     return size_t(code);
   }
 
@@ -1148,6 +1152,13 @@
         return false;
       }
       return true;
+    } else if (name == "inherit-env") {
+      if (value != "true" && value != "false") {
+        ctx.error("invalid value: '" + value + "' for attribute '" +
+                  name + "'");
+        return false;
+      }
+      inheritEnv = value == "true";
     } else {
       return ExternalCommand::configureAttribute(ctx, name, value);
     }
@@ -1201,7 +1212,7 @@
     // Execute the command.
     if (!bsci.getExecutionQueue().executeProcess(
             context, std::vector<StringRef>(args.begin(), args.end()),
-            environment)) {
+            environment, /*inheritEnvironment=*/inheritEnv)) {
       // If the command failed, there is no need to gather dependencies.
       return false;
     }
diff --git a/lib/BuildSystem/BuildSystemFrontend.cpp b/lib/BuildSystem/BuildSystemFrontend.cpp
index 137e556..1078a19 100644
--- a/lib/BuildSystem/BuildSystemFrontend.cpp
+++ b/lib/BuildSystem/BuildSystemFrontend.cpp
@@ -313,7 +313,8 @@
   
   if (impl->invocation.useSerialBuild) {
     return std::unique_ptr<BuildExecutionQueue>(
-        createLaneBasedExecutionQueue(impl->executionQueueDelegate, 1));
+        createLaneBasedExecutionQueue(impl->executionQueueDelegate, 1,
+                                      impl->invocation.environment));
   }
     
   // Get the number of CPUs to use.
@@ -327,7 +328,8 @@
   }
     
   return std::unique_ptr<BuildExecutionQueue>(
-      createLaneBasedExecutionQueue(impl->executionQueueDelegate, numLanes));
+      createLaneBasedExecutionQueue(impl->executionQueueDelegate, numLanes,
+                                    impl->invocation.environment));
 }
 
 bool BuildSystemFrontendDelegate::isCancelled() {
diff --git a/lib/BuildSystem/LaneBasedExecutionQueue.cpp b/lib/BuildSystem/LaneBasedExecutionQueue.cpp
index 8977150..d19103c 100644
--- a/lib/BuildSystem/LaneBasedExecutionQueue.cpp
+++ b/lib/BuildSystem/LaneBasedExecutionQueue.cpp
@@ -17,6 +17,7 @@
 #include "llbuild/Basic/PlatformUtility.h"
 
 #include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/Hashing.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/Twine.h"
@@ -43,11 +44,13 @@
 using namespace llbuild;
 using namespace llbuild::buildsystem;
 
-#if !defined(_WIN32)
-extern "C" {
-  extern char **environ;
+namespace std {
+  template<> struct hash<llvm::StringRef> {
+    size_t operator()(const StringRef& value) const {
+      return size_t(hash_value(value));
+    }
+  };
 }
-#endif
 
 namespace {
 
@@ -82,6 +85,9 @@
   std::mutex queueCompleteMutex;
   bool queueComplete { false };
 
+  /// The base environment.
+  const char* const* environment;
+  
   void executeLane(unsigned laneNumber) {
     // Set the thread name, if available.
 #if defined(__APPLE__)
@@ -153,8 +159,10 @@
 
 public:
   LaneBasedExecutionQueue(BuildExecutionQueueDelegate& delegate,
-                          unsigned numLanes)
-      : BuildExecutionQueue(delegate), numLanes(numLanes)
+                          unsigned numLanes,
+                          const char* const* environment)
+      : BuildExecutionQueue(delegate), numLanes(numLanes),
+        environment(environment)
   {
     for (unsigned i = 0; i != numLanes; ++i) {
       lanes.push_back(std::unique_ptr<std::thread>(
@@ -208,7 +216,8 @@
   executeProcess(QueueJobContext* opaqueContext,
                  ArrayRef<StringRef> commandLine,
                  ArrayRef<std::pair<StringRef,
-                                    StringRef>> environment) override {
+                                    StringRef>> environment,
+                 bool inheritEnvironment) override {
     {
       std::unique_lock<std::mutex> lock(readyJobsMutex);
       // Do not execute new processes anymore after cancellation.
@@ -309,7 +318,7 @@
       posix_spawn_file_actions_adddup2(&fileActions, 2, 2);
     }
 
-    // Form the complete C-string command line.
+    // Form the complete C string command line.
     std::vector<std::string> argsStorage(
         commandLine.begin(), commandLine.end());
     std::vector<const char*> args(argsStorage.size() + 1);
@@ -320,24 +329,49 @@
 
     // Form the complete environment.
     std::vector<std::string> envStorage;
-    for (const auto& entry: environment) {
-      SmallString<256> assignment;
-      assignment += entry.first;
-      assignment += '=';
-      assignment += entry.second;
-      assignment += '\0';
-      envStorage.emplace_back(assignment.str());
-    }
-    std::vector<const char*> env(environment.size() + 1);
-    char* const* envp = nullptr;
+    std::vector<const char*> env;
+    const char* const* envp = nullptr;
+
+    // If no additional environment is supplied, use the base environment.
     if (environment.empty()) {
-      envp = environ;
+      envp = this->environment;
     } else {
-      for (size_t i = 0; i != envStorage.size(); ++i) {
-        env[i] = envStorage[i].c_str();
+      // Inherit the base environment, if desired.
+      if (inheritEnvironment) {
+        std::unordered_set<StringRef> overriddenKeys{};
+        // Compute the set of strings which are overridden in the process
+        // environment.
+        for (const auto& entry: environment) {
+          overriddenKeys.insert(entry.first);
+        }
+
+        // Form the complete environment by adding the base key value pairs
+        // which are not overridden, then adding all of the overridden ones.
+        for (const char* const* p = this->environment; *p != nullptr; ++p) {
+          // Find the key.
+          auto key = StringRef(*p).split('=').first;
+          if (!overriddenKeys.count(key)) {
+            env.emplace_back(*p);
+          }
+        }
       }
-      env[envStorage.size()] = nullptr;
-      envp = const_cast<char**>(env.data());
+
+      // Add the requested environment.
+      for (const auto& entry: environment) {
+        SmallString<256> assignment;
+        assignment += entry.first;
+        assignment += '=';
+        assignment += entry.second;
+        assignment += '\0';
+        envStorage.emplace_back(assignment.str());
+      }
+      // We must do this in a second pass, once the entries are guaranteed not
+      // to move.
+      for (const auto& entry: envStorage) {
+        env.emplace_back(entry.c_str());
+      }
+      env.emplace_back(nullptr);
+      envp = env.data();
     }
 
     // Resolve the executable path, if necessary.
@@ -360,7 +394,7 @@
 
       if (posix_spawn(&pid, args[0], /*file_actions=*/&fileActions,
                       /*attrp=*/&attributes, const_cast<char**>(args.data()),
-                      envp) != 0) {
+                      const_cast<char* const*>(envp)) != 0) {
         getDelegate().commandProcessHadError(
             context.job.getForCommand(), handle,
             Twine("unable to spawn process (") + strerror(errno) + ")");
@@ -439,8 +473,18 @@
 
 }
 
+#if !defined(_WIN32)
+extern "C" {
+  extern char **environ;
+}
+#endif
+
 BuildExecutionQueue*
 llbuild::buildsystem::createLaneBasedExecutionQueue(
-    BuildExecutionQueueDelegate& delegate, int numLanes) {
-  return new LaneBasedExecutionQueue(delegate, numLanes);
+    BuildExecutionQueueDelegate& delegate, int numLanes,
+    const char* const* environment) {
+  if (!environment) {
+    environment = const_cast<const char* const*>(environ);
+  }
+  return new LaneBasedExecutionQueue(delegate, numLanes, environment);
 }
diff --git a/products/libllbuild/BuildSystem-C-API.cpp b/products/libllbuild/BuildSystem-C-API.cpp
index ce7d411..4931759 100644
--- a/products/libllbuild/BuildSystem-C-API.cpp
+++ b/products/libllbuild/BuildSystem-C-API.cpp
@@ -328,7 +328,9 @@
     invocation.buildFilePath =
       cAPIInvocation.buildFilePath ? cAPIInvocation.buildFilePath : "";
     invocation.dbPath = cAPIInvocation.dbPath ? cAPIInvocation.dbPath : "";
-    invocation.traceFilePath = cAPIInvocation.traceFilePath ? cAPIInvocation.traceFilePath : "";
+    invocation.traceFilePath = (
+        cAPIInvocation.traceFilePath ? cAPIInvocation.traceFilePath : "");
+    invocation.environment = cAPIInvocation.environment;
     invocation.useSerialBuild = cAPIInvocation.useSerialBuild;
     invocation.showVerboseStatus = cAPIInvocation.showVerboseStatus;
 
diff --git a/products/libllbuild/public-api/llbuild/buildsystem.h b/products/libllbuild/public-api/llbuild/buildsystem.h
index 61b16b2..b35d76e 100644
--- a/products/libllbuild/public-api/llbuild/buildsystem.h
+++ b/products/libllbuild/public-api/llbuild/buildsystem.h
@@ -124,7 +124,18 @@
 
   /// The path of the build trace output file to use, if any.
   const char* traceFilePath;
-    
+
+  /// The base environment to use when executing subprocesses.
+  ///
+  /// The format is expected to match that of `::main()`, i.e. a null-terminated
+  /// array of pointers to null terminated C strings.
+  ///
+  /// If empty, the environment of the calling process will be used.
+  ///
+  /// The environment is *not* copied by the build invocation, and must remain
+  /// alive for the lifetime of any build using this invocation.
+  const char* const* environment;
+  
   /// Whether to show verbose output.
   //
   // FIXME: This doesn't belong here, move once the status is fully delegated.
@@ -247,7 +258,8 @@
   ///
   /// The system guarantees that all such calls will be paired with a
   /// corresponding \see commandFinished() call.
-  bool (*should_command_start)(void* context, llb_buildsystem_command_t* command);
+  bool (*should_command_start)(void* context,
+                               llb_buildsystem_command_t* command);
 
   /// Called when a command has been started.
   ///
@@ -323,6 +335,7 @@
 /// It is an unchecked error for the client to request multiple builds
 /// concurrently.
 ///
+/// \param key The key to build.
 /// \returns True on success, or false if the build was aborted (for example, if
 /// a cycle was discovered).
 LLBUILD_EXPORT bool
@@ -403,8 +416,6 @@
   ///
   /// The contents *MUST* be returned in a new buffer allocated with \see
   /// malloc().
-  //
-  // FIXME: We need to use a better data type than a uint64_t here.
   void (*get_signature)(void* context, llb_buildsystem_command_t* command,
                         llb_data_t* data_out);
 
diff --git a/products/libllbuild/public-api/llbuild/llbuild.h b/products/libllbuild/public-api/llbuild/llbuild.h
index 6ebee03..41103fe 100644
--- a/products/libllbuild/public-api/llbuild/llbuild.h
+++ b/products/libllbuild/public-api/llbuild/llbuild.h
@@ -31,6 +31,18 @@
 #define LLBUILD_EXPORT extern
 #endif
 
+/// A monotonically increasing indicator of the llbuild API version.
+///
+/// The llbuild API is *not* stable. This value allows clients to conditionally
+/// compile for multiple versions of the API.
+///
+/// Version History:
+///
+/// 1: Added `environment` parameter to llb_buildsystem_invocation_t.
+///
+/// 0: Pre-history
+#define LLBUILD_API_VERSION 1
+
 /// Get the full version of the llbuild library.
 LLBUILD_EXPORT const char* llb_get_full_version_string(void);
 
diff --git a/tests/BuildSystem/Build/basic.llbuild b/tests/BuildSystem/Build/basic.llbuild
index 03ab159..6110430 100644
--- a/tests/BuildSystem/Build/basic.llbuild
+++ b/tests/BuildSystem/Build/basic.llbuild
@@ -9,8 +9,11 @@
 # RUN: diff "%t.build/input A" %t.build/output
 #
 # CHECK: /usr/bin/env
+# CHECK: ENV_KEY=ENV_VALUE_1
 # CHECK-NOT: PATH=
-# CHECK: ENV_KEY=ENV_VALUE
+# CHECK-NEXT: /usr/bin/env
+# CHECK: ENV_KEY=ENV_VALUE_2
+# CHECK: PATH=random-overridden-path
 # CHECK: cp 'input A' output
 
 # Check the engine trace.
@@ -21,7 +24,7 @@
 # CHECK-TRACE: "new-rule", "R1", "Tbasic"
 # CHECK-TRACE: "new-rule", "R2", "Noutput"
 # CHECK-TRACE: "new-rule", "R3", "Ccp-output"
-# CHECK-TRACE: "new-rule", "R4", "N<env>"
+# CHECK-TRACE: "new-rule", "R4", "N<env-2>"
 # CHECK-TRACE: "build-ended"
 
 # Check that a null build does nothing.
@@ -59,16 +62,26 @@
 default: basic
 
 commands:
-  "<env>":
+  "<env-1>":
     tool: shell
-    outputs: ["<env>"]
+    outputs: ["<env-1>"]
     args: ["/usr/bin/env"]
     env:
-      ENV_KEY: ENV_VALUE
-    
+      ENV_KEY: ENV_VALUE_1
+    inherit-env: false
+
+  "<env-2>":
+    tool: shell
+    inputs: ["<env-1>"]
+    outputs: ["<env-2>"]
+    args: /usr/bin/env | /usr/bin/sort
+    env:
+      ENV_KEY: ENV_VALUE_2
+      PATH: random-overridden-path
+
   cp-output:
     tool: shell
-    inputs: ["input A", "<env>"]
+    inputs: ["input A", "<env-1>", "<env-2>"]
     outputs: ["output"]
     # FIXME: Design a limited mechanism for substitution. Might be tool specific.
     args: ["cp", "input A", "output"]
diff --git a/unittests/BuildSystem/LaneBasedExecutionQueueTest.cpp b/unittests/BuildSystem/LaneBasedExecutionQueueTest.cpp
index c2107a9..5b5c77b 100644
--- a/unittests/BuildSystem/LaneBasedExecutionQueueTest.cpp
+++ b/unittests/BuildSystem/LaneBasedExecutionQueueTest.cpp
@@ -37,10 +37,15 @@
 
     virtual void commandJobStarted(Command* command) override {}
     virtual void commandJobFinished(Command* command) override {}
-    virtual void commandProcessStarted(Command* command, ProcessHandle handle) override {}
-    virtual void commandProcessHadError(Command* command, ProcessHandle handle, const Twine& message) override {}
-    virtual void commandProcessHadOutput(Command* command, ProcessHandle handle, StringRef data) override {}
-    virtual void commandProcessFinished(Command* command, ProcessHandle handle, CommandResult result, int exitStatus) override {}
+    virtual void commandProcessStarted(Command* command,
+                                       ProcessHandle handle) override {}
+    virtual void commandProcessHadError(Command* command, ProcessHandle handle,
+                                        const Twine& message) override {}
+    virtual void commandProcessHadOutput(Command* command, ProcessHandle handle,
+                                         StringRef data) override {}
+    virtual void commandProcessFinished(Command* command, ProcessHandle handle,
+                                        CommandResult result,
+                                        int exitStatus) override {}
   };
 
   TEST(LaneBasedExecutionQueueTest, basic) {
@@ -48,7 +53,8 @@
     std::unique_ptr<FileSystem> fs = createLocalFileSystem();
     TmpDir tempDir{"LaneBasedExecutionQueueTest"};
     std::string outputFile = tempDir.str() + "/yes-output.txt";
-    auto queue = std::unique_ptr<BuildExecutionQueue>(createLaneBasedExecutionQueue(delegate, 2));
+    auto queue = std::unique_ptr<BuildExecutionQueue>(
+        createLaneBasedExecutionQueue(delegate, 2, /*environment=*/nullptr));
 
     auto fn = [&outputFile, &queue](QueueJobContext* context) {
       queue->executeShellCommand(context, "yes >" + outputFile);
@@ -56,11 +62,13 @@
 
     queue->addJob(QueueJob((Command*)0x1, fn));
 
-    // Busy wait until `outputFile` appears which indicates that `yes` is running
+    // Busy wait until `outputFile` appears which indicates that `yes` is
+    // running.
     time_t start = ::time(NULL);
     while (fs->getFileInfo(outputFile).isMissing()) {
       if (::time(NULL) > start + 5) {
-         // We can't fail gracefully because the `LaneBasedExecutionQueue` will always wait for spawned processes to exit
+        // We can't fail gracefully because the `LaneBasedExecutionQueue` will
+        // always wait for spawned processes to exit
         abort();
       }
     }
@@ -71,7 +79,8 @@
 
   TEST(LaneBasedExecutionQueueTest, exhaustsQueueAfterCancellation) {
     DummyDelegate delegate;
-    auto queue = std::unique_ptr<BuildExecutionQueue>(createLaneBasedExecutionQueue(delegate, 1));
+    auto queue = std::unique_ptr<BuildExecutionQueue>(
+        createLaneBasedExecutionQueue(delegate, 1, /*environment=*/nullptr));
 
     bool buildStarted { false };
     std::condition_variable buildStartedCondition;