Introduce CommandEnvironment and pass it instead of BlazeRuntime.

The very long term plan is to make BlazeRuntime immutable, so that we can
run multiple commands in the same server.

--
MOS_MIGRATED_REVID=103080946
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommand.java
index 61f46a8..c050043 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommand.java
@@ -39,7 +39,7 @@
    * shut down and the exit status will be 0 (in case the shutdown succeeds
    * without error).
    *
-   * @param runtime The Blaze runtime requesting the execution of the command
+   * @param env The environment for the current command invocation
    * @param options A parsed options instance initialized with the values for
    *     the options specified in {@link Command#options()}.
    *
@@ -47,7 +47,7 @@
    * @throws BlazeCommandDispatcher.ShutdownBlazeServerException Indicates
    *     that the command wants to shutdown the Blaze server.
    */
-  ExitCode exec(BlazeRuntime runtime, OptionsProvider options)
+  ExitCode exec(CommandEnvironment env, OptionsProvider options)
       throws BlazeCommandDispatcher.ShutdownBlazeServerException;
 
   /**
@@ -55,9 +55,10 @@
    * requirements. This method is called after all command-line and rc file options have been
    * parsed.
    *
-   * @param runtime The Blaze runtime requesting the execution of the command
+   * @param env the command environment of the currently running command
+   * @param optionsParser the options parser for the current command
    *
    * @throws AbruptExitException if something went wrong
    */
-  void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) throws AbruptExitException;
+  void editOptions(CommandEnvironment env, OptionsParser optionsParser) throws AbruptExitException;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
index 9b901e6..cb0a0f9 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java
@@ -239,7 +239,7 @@
     // the command's execution - that's why we do this separately,
     // rather than in runtime.beforeCommand().
     runtime.getTimestampGranularityMonitor().setCommandStartTime();
-    runtime.initEventBus();
+    CommandEnvironment env = runtime.initCommand();
 
     if (args.isEmpty()) { // Default to help command if no arguments specified.
       args = HELP_COMMAND;
@@ -262,7 +262,7 @@
     AbruptExitException exitCausingException = null;
     for (BlazeModule module : runtime.getBlazeModules()) {
       try {
-        module.beforeCommand(runtime, commandAnnotation);
+        module.beforeCommand(runtime, commandAnnotation, env);
       } catch (AbruptExitException e) {
         // Don't let one module's complaints prevent the other modules from doing necessary
         // setup. We promised to call beforeCommand exactly once per-module before each command
@@ -340,7 +340,7 @@
     PrintStream savedErr = System.err;
 
     EventHandler handler = createEventHandler(outErr, eventHandlerOptions);
-    Reporter reporter = runtime.getReporter();
+    Reporter reporter = env.getReporter();
     reporter.addHandler(handler);
 
     // We register an ANSI-allowing handler associated with {@code handler} so that ANSI control
@@ -372,9 +372,10 @@
 
       try {
         // Notify the BlazeRuntime, so it can do some initial setup.
-        runtime.beforeCommand(commandAnnotation, optionsParser, commonOptions, execStartTimeNanos);
+        runtime.beforeCommand(commandAnnotation, env, optionsParser, commonOptions,
+            execStartTimeNanos);
         // Allow the command to edit options after parsing:
-        command.editOptions(runtime, optionsParser);
+        command.editOptions(env, optionsParser);
       } catch (AbruptExitException e) {
         reporter.handle(Event.error(e.getMessage()));
         return e.getExitCode().getNumericExitCode();
@@ -385,8 +386,8 @@
         reporter.handle(Event.warn(warning));
       }
 
-      ExitCode outcome = command.exec(runtime, optionsParser);
-      outcome = runtime.precompleteCommand(outcome);
+      ExitCode outcome = command.exec(env, optionsParser);
+      outcome = runtime.precompleteCommand(env, outcome);
       numericExitCode = outcome.getNumericExitCode();
       return numericExitCode;
     } catch (ShutdownBlazeServerException e) {
@@ -400,7 +401,7 @@
           : ExitCode.BLAZE_INTERNAL_ERROR.getNumericExitCode();
       throw new ShutdownBlazeServerException(numericExitCode, e);
     } finally {
-      runtime.afterCommand(numericExitCode);
+      runtime.afterCommand(env, numericExitCode);
       // Swallow IOException, as we are already in a finally clause
       Flushables.flushQuietly(outErr.getOutputStream());
       Flushables.flushQuietly(outErr.getErrorStream());
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
index bfc1d8f..68eed55 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
@@ -221,8 +221,16 @@
    * Called before each command.
    */
   @SuppressWarnings("unused")
-  public void beforeCommand(BlazeRuntime blazeRuntime, Command command)
+  public void beforeCommand(Command command, CommandEnvironment env) throws AbruptExitException {
+  }
+
+  /**
+   * Called before each command.
+   */
+  @SuppressWarnings("unused")
+  public void beforeCommand(BlazeRuntime blazeRuntime, Command command, CommandEnvironment env)
       throws AbruptExitException {
+    beforeCommand(command, env);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
index 7e4e41c..c1ec448 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
@@ -51,7 +51,6 @@
 import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
 import com.google.devtools.build.lib.analysis.config.DefaultsPackage;
 import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
-import com.google.devtools.build.lib.buildtool.BuildTool;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.OutputFilter;
 import com.google.devtools.build.lib.events.Reporter;
@@ -154,10 +153,11 @@
 import javax.annotation.Nullable;
 
 /**
- * The BlazeRuntime class encapsulates the runtime settings and services that
- * are available to most parts of any Blaze application for the duration of the
- * batch run or server lifetime. A single instance of this runtime will exist
- * and will be passed around as needed.
+ * The BlazeRuntime class encapsulates the immutable configuration of the current instance. These
+ * runtime settings and services are available to most parts of any Blaze application for the
+ * duration of the batch run or server lifetime.
+ *
+ * <p>The parts specific to the current command are stored in {@link CommandEnvironment}.
  */
 public final class BlazeRuntime {
   public static final String DO_NOT_BUILD_FILE_NAME = "DO_NOT_BUILD_HERE";
@@ -177,7 +177,6 @@
   private final SkyframeExecutor skyframeExecutor;
 
   private final Reporter reporter;
-  private EventBus eventBus;
   private final LoadingPhaseRunner loadingPhaseRunner;
   private final PackageFactory packageFactory;
   private final PackageRootResolver packageRootResolver;
@@ -187,7 +186,6 @@
   private ActionCache actionCache;
   private final TimestampGranularityMonitor timestampGranularityMonitor;
   private final Clock clock;
-  private final BuildTool buildTool;
 
   private OutputService outputService;
 
@@ -270,8 +268,6 @@
 
     this.eventBusExceptionHandler = eventBusExceptionHandler;
     this.blazeModuleEnvironment = new BlazeModuleEnvironment();
-    this.buildTool = new BuildTool(this);
-    initEventBus();
 
     if (inWorkspace()) {
       writeOutputBaseReadmeFile();
@@ -311,26 +307,22 @@
     return outputFileSystem;
   }
 
-  @VisibleForTesting
-  public void initEventBus() {
-    setEventBus(new EventBus(eventBusExceptionHandler));
+  public CommandEnvironment initCommand() {
+    EventBus eventBus = new EventBus(eventBusExceptionHandler);
+    skyframeExecutor.setEventBus(eventBus);
+    return new CommandEnvironment(this, eventBus);
   }
 
   private void clearEventBus() {
     // EventBus does not have an unregister() method, so this is how we release memory associated
     // with handlers.
-    setEventBus(null);
-  }
-
-  private void setEventBus(EventBus eventBus) {
-    this.eventBus = eventBus;
-    skyframeExecutor.setEventBus(eventBus);
+    skyframeExecutor.setEventBus(null);
   }
 
   /**
    * Conditionally enable profiling.
    */
-  private final boolean initProfiler(CommonCommandOptions options,
+  private final boolean initProfiler(CommandEnvironment env, CommonCommandOptions options,
       UUID buildID, long execStartTimeNanos) {
     OutputStream out = null;
     boolean recordFullProfilerData = false;
@@ -342,7 +334,7 @@
 
         recordFullProfilerData = options.recordFullProfilerData;
         out = new BufferedOutputStream(profilePath.getOutputStream(), 1024 * 1024);
-        getReporter().handle(Event.info("Writing profile data to '" + profilePath + "'"));
+        env.getReporter().handle(Event.info("Writing profile data to '" + profilePath + "'"));
         profiledTasks = ProfiledTaskKinds.ALL;
       } else if (options.alwaysProfileSlowOperations) {
         recordFullProfilerData = false;
@@ -357,7 +349,7 @@
         return true;
       }
     } catch (IOException e) {
-      getReporter().handle(Event.error("Error while creating profile file: " + e.getMessage()));
+      env.getReporter().handle(Event.error("Error while creating profile file: " + e.getMessage()));
     }
     return false;
   }
@@ -517,13 +509,6 @@
     return reporter;
   }
 
-  /**
-   * Returns the current event bus. Only valid within the scope of a single Blaze command.
-   */
-  public EventBus getEventBus() {
-    return eventBus;
-  }
-
   public BinTools getBinTools() {
     return binTools;
   }
@@ -542,13 +527,6 @@
     return packageFactory;
   }
 
-  /**
-   * Returns the build tool.
-   */
-  public BuildTool getBuildTool() {
-    return buildTool;
-  }
-
   public ImmutableList<OutputFormatter> getQueryOutputFormatters() {
     ImmutableList.Builder<OutputFormatter> result = ImmutableList.builder();
     result.addAll(OutputFormatter.getDefaultFormatters());
@@ -702,12 +680,12 @@
    * @param options The CommonCommandOptions used by every command.
    * @throws AbruptExitException if this command is unsuitable to be run as specified
    */
-  void beforeCommand(Command command, OptionsParser optionsParser,
+  void beforeCommand(Command command, CommandEnvironment env, OptionsParser optionsParser,
       CommonCommandOptions options, long execStartTimeNanos)
       throws AbruptExitException {
     commandStartTime -= options.startupTime;
 
-    eventBus.post(new GotOptionsEvent(startupOptionsProvider,
+    env.getEventBus().post(new GotOptionsEvent(startupOptionsProvider,
         optionsParser));
     throwPendingException();
 
@@ -753,7 +731,7 @@
     // Conditionally enable profiling
     // We need to compensate for launchTimeNanos (measurements taken outside of the jvm).
     long startupTimeNanos = options.startupTime * 1000000L;
-    if (initProfiler(options, this.getCommandId(), execStartTimeNanos - startupTimeNanos)) {
+    if (initProfiler(env, options, this.getCommandId(), execStartTimeNanos - startupTimeNanos)) {
       Profiler profiler = Profiler.instance();
 
       // Instead of logEvent() we're calling the low level function to pass the timings we took in
@@ -770,7 +748,7 @@
       try {
         MemoryProfiler.instance().start(memoryProfilePath.getOutputStream());
       } catch (IOException e) {
-        getReporter().handle(
+        env.getReporter().handle(
             Event.error("Error while creating memory profile file: " + e.getMessage()));
       }
     }
@@ -802,7 +780,8 @@
       module.handleOptions(optionsParser);
     }
 
-    eventBus.post(new CommandStartEvent(command.name(), commandId, clientEnv, workingDirectory));
+    env.getEventBus().post(
+        new CommandStartEvent(command.name(), commandId, clientEnv, workingDirectory));
     // Initialize exit code to dummy value for afterCommand.
     storedExitCode.set(ExitCode.RESERVED.getNumericExitCode());
   }
@@ -811,8 +790,8 @@
    * Hook method called by the BlazeCommandDispatcher right before the dispatch
    * of each command ends (while its outcome can still be modified).
    */
-  ExitCode precompleteCommand(ExitCode originalExit) {
-    eventBus.post(new CommandPrecompleteEvent(originalExit));
+  ExitCode precompleteCommand(CommandEnvironment env, ExitCode originalExit) {
+    env.getEventBus().post(new CommandPrecompleteEvent(originalExit));
     // If Blaze did not suffer an infrastructure failure, check for errors in modules.
     ExitCode exitCode = originalExit;
     if (!originalExit.isInfrastructureFailure() && pendingException != null) {
@@ -833,7 +812,7 @@
       // thread won the race (unlikely, but possible), this may be incorrectly logged as a success.
       return;
     }
-    eventBus.post(new CommandCompleteEvent(exitCode));
+    skyframeExecutor.getEventBus().post(new CommandCompleteEvent(exitCode));
   }
 
   /**
@@ -841,9 +820,9 @@
    * command.
    */
   @VisibleForTesting
-  public void afterCommand(int exitCode) {
+  public void afterCommand(CommandEnvironment env, int exitCode) {
     // Remove any filters that the command might have added to the reporter.
-    getReporter().setOutputFilter(OutputFilter.OUTPUT_EVERYTHING);
+    env.getReporter().setOutputFilter(OutputFilter.OUTPUT_EVERYTHING);
 
     notifyCommandComplete(exitCode);
 
@@ -857,7 +836,7 @@
       Profiler.instance().stop();
       MemoryProfiler.instance().stop();
     } catch (IOException e) {
-      getReporter().handle(Event.error("Error while writing profile file: " + e.getMessage()));
+      env.getReporter().handle(Event.error("Error while writing profile file: " + e.getMessage()));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
index d0b70f1..337f97f 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java
@@ -41,9 +41,9 @@
   private Reporter reporter;
 
   @Override
-  public void beforeCommand(BlazeRuntime runtime, Command command) {
-    this.reporter = runtime.getReporter();
-    this.eventBus = runtime.getEventBus();
+  public void beforeCommand(Command command, CommandEnvironment env) {
+    this.reporter = env.getReporter();
+    this.eventBus = env.getEventBus();
     eventBus.register(this);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
new file mode 100644
index 0000000..02f7652
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
@@ -0,0 +1,58 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.runtime;
+
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.events.Reporter;
+
+import java.util.Map;
+
+/**
+ * Encapsulates the state needed for a single command. The environment is dropped when the current
+ * command is done and all corresponding objects are garbage collected.
+ */
+public final class CommandEnvironment {
+  private final BlazeRuntime runtime;
+  private final EventBus eventBus;
+
+  public CommandEnvironment(BlazeRuntime runtime, EventBus eventBus) {
+    this.runtime = runtime;
+    this.eventBus = eventBus;
+  }
+
+  public BlazeRuntime getRuntime() {
+    return runtime;
+  }
+
+  public BlazeDirectories getDirectories() {
+    return runtime.getDirectories();
+  }
+
+  /**
+   * Returns the reporter for events.
+   */
+  public Reporter getReporter() {
+    return runtime.getReporter();
+  }
+
+  public EventBus getEventBus() {
+    return eventBus;
+  }
+
+  public Map<String, String> getClientEnv() {
+    return runtime.getClientEnv();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java
index 94a0ada..0082d71 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java
@@ -17,12 +17,14 @@
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.buildtool.BuildRequest.BuildRequestOptions;
+import com.google.devtools.build.lib.buildtool.BuildTool;
 import com.google.devtools.build.lib.exec.ExecutionOptions;
 import com.google.devtools.build.lib.pkgcache.LoadingPhaseRunner;
 import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
 import com.google.devtools.build.lib.runtime.BlazeCommand;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.common.options.OptionsParser;
@@ -51,20 +53,21 @@
 public final class BuildCommand implements BlazeCommand {
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser)
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser)
       throws AbruptExitException {
-    ProjectFileSupport.handleProjectFiles(runtime, optionsParser, "build");
+    ProjectFileSupport.handleProjectFiles(env, optionsParser, "build");
   }
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+    BlazeRuntime runtime = env.getRuntime();
     List<String> targets = ProjectFileSupport.getTargets(runtime, options);
 
     BuildRequest request = BuildRequest.create(
         getClass().getAnnotation(Command.class).name(), options,
         runtime.getStartupOptionsProvider(),
         targets,
-        runtime.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
-    return runtime.getBuildTool().processRequest(request, null).getExitCondition();
+        env.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
+    return new BuildTool(env).processRequest(request, null).getExitCondition();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/CanonicalizeCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/CanonicalizeCommand.java
index 82d503f..1b5b1cd 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/CanonicalizeCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/CanonicalizeCommand.java
@@ -19,6 +19,7 @@
 import com.google.devtools.build.lib.runtime.BlazeCommandUtils;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionsBase;
@@ -51,11 +52,12 @@
   }
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+    BlazeRuntime runtime = env.getRuntime();
     String commandName = options.getOptions(Options.class).forCommand;
     BlazeCommand command = runtime.getCommandMap().get(commandName);
     if (command == null) {
-      runtime.getReporter().handle(Event.error("Not a valid command: '" + commandName
+      env.getReporter().handle(Event.error("Not a valid command: '" + commandName
           + "' (should be one of " + Joiner.on(", ").join(runtime.getCommandMap().keySet()) + ")"));
       return ExitCode.COMMAND_LINE_ERROR;
     }
@@ -65,15 +67,15 @@
     try {
       List<String> result = OptionsParser.canonicalize(optionsClasses, options.getResidue());
       for (String piece : result) {
-        runtime.getReporter().getOutErr().printOutLn(piece);
+        env.getReporter().getOutErr().printOutLn(piece);
       }
     } catch (OptionsParsingException e) {
-      runtime.getReporter().handle(Event.error(e.getMessage()));
+      env.getReporter().handle(Event.error(e.getMessage()));
       return ExitCode.COMMAND_LINE_ERROR;
     }
     return ExitCode.SUCCESS;
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
 }
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 5be4f90..f8d236c 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
@@ -22,6 +22,7 @@
 import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.ShutdownBlazeServerException;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.util.CommandBuilder;
 import com.google.devtools.build.lib.util.ExitCode;
@@ -83,15 +84,16 @@
   private static Logger LOG = Logger.getLogger(CleanCommand.class.getName());
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options)
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options)
       throws ShutdownBlazeServerException {
+    BlazeRuntime runtime = env.getRuntime();
     Options cleanOptions = options.getOptions(Options.class);
     cleanOptions.expunge_async = cleanOptions.cleanStyle.equals("expunge_async");
     cleanOptions.expunge = cleanOptions.cleanStyle.equals("expunge");
 
     if (!cleanOptions.expunge && !cleanOptions.expunge_async
         && !cleanOptions.cleanStyle.isEmpty()) {
-      runtime.getReporter().handle(Event.error(
+      env.getReporter().handle(Event.error(
           null, "Invalid clean_style value '" + cleanOptions.cleanStyle + "'"));
       return ExitCode.COMMAND_LINE_ERROR;
     }
@@ -101,27 +103,28 @@
         "Starting clean (this may take a while). " +
             "Consider using --expunge_async if the clean takes more than several minutes.";
 
-    runtime.getReporter().handle(Event.info(null/*location*/, cleanBanner));
+    env.getReporter().handle(Event.info(null/*location*/, cleanBanner));
     try {
       String symlinkPrefix =
           options.getOptions(BuildRequest.BuildRequestOptions.class).symlinkPrefix;
-      actuallyClean(runtime, runtime.getOutputBase(), cleanOptions, symlinkPrefix);
+      actuallyClean(env, runtime.getOutputBase(), cleanOptions, symlinkPrefix);
       return ExitCode.SUCCESS;
     } catch (IOException e) {
-      runtime.getReporter().handle(Event.error(e.getMessage()));
+      env.getReporter().handle(Event.error(e.getMessage()));
       return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
     } catch (CommandException | ExecException e) {
-      runtime.getReporter().handle(Event.error(e.getMessage()));
+      env.getReporter().handle(Event.error(e.getMessage()));
       return ExitCode.RUN_FAILURE;
     } catch (InterruptedException e) {
-      runtime.getReporter().handle(Event.error("clean interrupted"));
+      env.getReporter().handle(Event.error("clean interrupted"));
       return ExitCode.INTERRUPTED;
     }
   }
 
-  private void actuallyClean(BlazeRuntime runtime,
+  private void actuallyClean(CommandEnvironment env,
       Path outputBase, Options cleanOptions, String symlinkPrefix) throws IOException,
       ShutdownBlazeServerException, CommandException, ExecException, InterruptedException {
+    BlazeRuntime runtime = env.getRuntime();
     if (runtime.getOutputService() != null) {
       runtime.getOutputService().clean();
     }
@@ -142,7 +145,7 @@
       // same file system, and therefore the mv will be atomic and fast.
       Path tempOutputBase = outputBase.getParentDirectory().getChild(tempBaseName);
       outputBase.renameTo(tempOutputBase);
-      runtime.getReporter().handle(Event.info(
+      env.getReporter().handle(Event.info(
           null, "Output base moved to " + tempOutputBase + " for deletion"));
 
       // Daemonize the shell and use the double-fork idiom to ensure that the shell
@@ -178,5 +181,5 @@
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java
index 1a0f8de..26eafa4 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java
@@ -22,6 +22,7 @@
 import com.google.devtools.build.lib.runtime.BlazeCommandUtils;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.common.options.EnumConverter;
@@ -55,7 +56,7 @@
 
   /**
    * NB! Any changes to this class must be kept in sync with anyOutput variable
-   * value in the {@link DumpCommand#exec(BlazeRuntime,OptionsProvider)} method below.
+   * value in the {@link DumpCommand#exec(CommandEnvironment,OptionsProvider)} method below.
    */
   public static class DumpOptions extends OptionsBase {
 
@@ -116,10 +117,11 @@
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+    BlazeRuntime runtime = env.getRuntime();
     DumpOptions dumpOptions = options.getOptions(DumpOptions.class);
 
     boolean anyOutput = dumpOptions.dumpPackages || dumpOptions.dumpVfs
@@ -131,14 +133,14 @@
       Collection<Class<? extends OptionsBase>> optionList = new ArrayList<>();
       optionList.add(DumpOptions.class);
 
-      runtime.getReporter().getOutErr().printErrLn(BlazeCommandUtils.expandHelpTopic(
+      env.getReporter().getOutErr().printErrLn(BlazeCommandUtils.expandHelpTopic(
           getClass().getAnnotation(Command.class).name(),
           getClass().getAnnotation(Command.class).help(),
           getClass(),
           optionList, categories, OptionsParser.HelpVerbosity.LONG));
       return ExitCode.ANALYSIS_FAILURE;
     }
-    PrintStream out = new PrintStream(runtime.getReporter().getOutErr().getOutputStream());
+    PrintStream out = new PrintStream(env.getReporter().getOutErr().getOutputStream());
     try {
       out.println("Warning: this information is intended for consumption by developers");
       out.println("only, and may change at any time.  Script against it at your own risk!");
@@ -158,7 +160,7 @@
 
       if (dumpOptions.dumpArtifacts) {
         success = false;
-        runtime.getReporter().handle(Event.error("Cannot dump artifacts in Skyframe full mode. "
+        env.getReporter().handle(Event.error("Cannot dump artifacts in Skyframe full mode. "
             + "Use --skyframe instead"));
       }
 
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java
index 4418f1e..7ee0ca6 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.io.OutErr;
 import com.google.devtools.common.options.Converters;
@@ -129,11 +130,12 @@
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
-    OutErr outErr = runtime.getReporter().getOutErr();
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+    BlazeRuntime runtime = env.getRuntime();
+    OutErr outErr = env.getReporter().getOutErr();
     Options helpOptions = options.getOptions(Options.class);
     if (options.getResidue().isEmpty()) {
       emitBlazeVersionInfo(outErr);
@@ -141,7 +143,7 @@
       return ExitCode.SUCCESS;
     }
     if (options.getResidue().size() != 1) {
-      runtime.getReporter().handle(Event.error("You must specify exactly one command"));
+      env.getReporter().handle(Event.error("You must specify exactly one command"));
       return ExitCode.COMMAND_LINE_ERROR;
     }
     String helpSubject = options.getResidue().get(0);
@@ -170,7 +172,7 @@
         outErr.printOut(BlazeRuleHelpPrinter.getRuleDoc(helpSubject, provider));
         return ExitCode.SUCCESS;
       } else {
-        runtime.getReporter().handle(Event.error(
+        env.getReporter().handle(Event.error(
             null, "'" + helpSubject + "' is neither a command nor a build rule"));
         return ExitCode.COMMAND_LINE_ERROR;
       }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
index 1a0566b..38f913e 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
@@ -38,6 +38,7 @@
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.OsUtils;
@@ -177,13 +178,14 @@
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) { }
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) { }
 
   @Override
-  public ExitCode exec(final BlazeRuntime runtime, final OptionsProvider optionsProvider) {
-    runtime.getReporter().switchToAnsiAllowingHandler();
+  public ExitCode exec(final CommandEnvironment env, final OptionsProvider optionsProvider) {
+    final BlazeRuntime runtime = env.getRuntime();
+    env.getReporter().switchToAnsiAllowingHandler();
     Options infoOptions = optionsProvider.getOptions(Options.class);
-    OutErr outErr = runtime.getReporter().getOutErr();
+    OutErr outErr = env.getReporter().getOutErr();
     // Creating a BuildConfiguration is expensive and often unnecessary. Delay the creation until
     // it is needed.
     Supplier<BuildConfiguration> configurationSupplier = new Supplier<BuildConfiguration>() {
@@ -206,13 +208,13 @@
               .getTargetConfigurations().get(0);
           return configuration;
         } catch (InvalidConfigurationException e) {
-          runtime.getReporter().handle(Event.error(e.getMessage()));
+          env.getReporter().handle(Event.error(e.getMessage()));
           throw new ExitCausingRuntimeException(ExitCode.COMMAND_LINE_ERROR);
         } catch (AbruptExitException e) {
           throw new ExitCausingRuntimeException("unknown error: " + e.getMessage(),
               e.getExitCode());
         } catch (InterruptedException e) {
-          runtime.getReporter().handle(Event.error("interrupted"));
+          env.getReporter().handle(Event.error("interrupted"));
           throw new ExitCausingRuntimeException(ExitCode.INTERRUPTED);
         }
       }
@@ -231,7 +233,7 @@
 
       List<String> residue = optionsProvider.getResidue();
       if (residue.size() > 1) {
-        runtime.getReporter().handle(Event.error("at most one key may be specified"));
+        env.getReporter().handle(Event.error("at most one key may be specified"));
         return ExitCode.COMMAND_LINE_ERROR;
       }
 
@@ -241,14 +243,14 @@
         if (items.containsKey(key)) {
           value = items.get(key).get(configurationSupplier);
         } else {
-          runtime.getReporter().handle(Event.error("unknown key: '" + key + "'"));
+          env.getReporter().handle(Event.error("unknown key: '" + key + "'"));
           return ExitCode.COMMAND_LINE_ERROR;
         }
         try {
           outErr.getOutputStream().write(value);
           outErr.getOutputStream().flush();
         } catch (IOException e) {
-          runtime.getReporter().handle(Event.error("Cannot write info block: " + e.getMessage()));
+          env.getReporter().handle(Event.error("Cannot write info block: " + e.getMessage()));
           return ExitCode.ANALYSIS_FAILURE;
         }
       } else { // print them all
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/MobileInstallCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/MobileInstallCommand.java
index 13e2837..9701289 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/MobileInstallCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/MobileInstallCommand.java
@@ -16,12 +16,14 @@
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.analysis.OutputGroupProvider;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
+import com.google.devtools.build.lib.buildtool.BuildTool;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.rules.android.WriteAdbArgsAction;
 import com.google.devtools.build.lib.rules.android.WriteAdbArgsAction.StartType;
 import com.google.devtools.build.lib.runtime.BlazeCommand;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.common.options.Option;
@@ -65,11 +67,12 @@
   }
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+    BlazeRuntime runtime = env.getRuntime();
     Options mobileInstallOptions = options.getOptions(Options.class);
     WriteAdbArgsAction.Options adbOptions = options.getOptions(WriteAdbArgsAction.Options.class);
     if (adbOptions.start == StartType.WARM && !mobileInstallOptions.incremental) {
-      runtime.getReporter().handle(Event.warn(
+      env.getReporter().handle(Event.warn(
          "Warm start is enabled, but will have no effect on a non-incremental build"));
     }
 
@@ -77,12 +80,12 @@
     BuildRequest request = BuildRequest.create(
         this.getClass().getAnnotation(Command.class).name(), options,
         runtime.getStartupOptionsProvider(), targets,
-        runtime.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
-    return runtime.getBuildTool().processRequest(request, null).getExitCondition();
+        env.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
+    return new BuildTool(env).processRequest(request, null).getExitCondition();
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser)
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser)
       throws AbruptExitException {
     try {
       String outputGroup =
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java
index 0992aad..89d6bbb 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProfileCommand.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.runtime.BlazeCommand;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.util.StringUtil;
 import com.google.devtools.build.lib.util.TimeUtilities;
@@ -138,10 +139,11 @@
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
 
   @Override
-  public ExitCode exec(final BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(final CommandEnvironment env, OptionsProvider options) {
+    final BlazeRuntime runtime = env.getRuntime();
     ProfileOptions opts =
         options.getOptions(ProfileOptions.class);
 
@@ -160,9 +162,9 @@
       }
     };
 
-    PrintStream out = new PrintStream(runtime.getReporter().getOutErr().getOutputStream());
+    PrintStream out = new PrintStream(env.getReporter().getOutErr().getOutputStream());
     try {
-      runtime.getReporter().handle(Event.warn(
+      env.getReporter().handle(Event.warn(
           null, "This information is intended for consumption by Blaze developers"
               + " only, and may change at any time.  Script against it at your own risk"));
 
@@ -177,7 +179,7 @@
             Path htmlFile =
                 profileFile.getParentDirectory().getChild(profileFile.getBaseName() + ".html");
 
-            runtime.getReporter().handle(Event.info("Creating HTML output in " + htmlFile));
+            env.getReporter().handle(Event.info("Creating HTML output in " + htmlFile));
 
             HtmlCreator.createHtml(
                 info,
@@ -189,7 +191,7 @@
             createText(runtime, info, out, opts);
           }
         } catch (IOException e) {
-          runtime.getReporter().handle(Event.error(
+          env.getReporter().handle(Event.error(
               null, "Failed to process file " + name + ": " + e.getMessage()));
         }
       }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProjectFileSupport.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProjectFileSupport.java
index e3beaa4..63552b4 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/ProjectFileSupport.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ProjectFileSupport.java
@@ -18,6 +18,7 @@
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
 import com.google.devtools.build.lib.runtime.BlazeCommand;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.runtime.CommonCommandOptions;
 import com.google.devtools.build.lib.runtime.ProjectFile;
 import com.google.devtools.build.lib.util.AbruptExitException;
@@ -44,8 +45,9 @@
    * accordingly. If project files cannot be read or if they contain unparsable options, or if they
    * are not enabled, then it throws an exception instead.
    */
-  public static void handleProjectFiles(BlazeRuntime runtime, OptionsParser optionsParser,
+  public static void handleProjectFiles(CommandEnvironment env, OptionsParser optionsParser,
       String command) throws AbruptExitException {
+    BlazeRuntime runtime = env.getRuntime();
     List<String> targets = optionsParser.getResidue();
     ProjectFile.Provider projectFileProvider = runtime.getProjectFileProvider();
     if (projectFileProvider != null && !targets.isEmpty()
@@ -65,11 +67,11 @@
       List<Path> packagePath = PathPackageLocator.create(
           runtime.getOutputBase(),
           optionsParser.getOptions(PackageCacheOptions.class).packagePath,
-          runtime.getReporter(),
+          env.getReporter(),
           runtime.getWorkspace(),
           runtime.getWorkingDirectory()).getPathEntries();
       ProjectFile projectFile = projectFileProvider.getProjectFile(packagePath, projectFilePath);
-      runtime.getReporter().handle(Event.info("Using " + projectFile.getName()));
+      env.getReporter().handle(Event.info("Using " + projectFile.getName()));
 
       try {
         optionsParser.parse(
@@ -77,7 +79,7 @@
       } catch (OptionsParsingException e) {
         throw new AbruptExitException(e.getMessage(), ExitCode.COMMAND_LINE_ERROR);
       }
-      runtime.getEventBus().post(new GotProjectFileEvent(projectFile.getName()));
+      env.getEventBus().post(new GotProjectFileEvent(projectFile.getName()));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
index 04e2b04..fe30b64 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/QueryCommand.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.common.options.OptionsParser;
@@ -59,7 +60,7 @@
 public final class QueryCommand implements BlazeCommand {
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) { }
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) { }
 
   /**
    * Exit codes:
@@ -70,7 +71,8 @@
    *        (only when --keep_going is in effect.)
    */
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+    BlazeRuntime runtime = env.getRuntime();
     QueryOptions queryOptions = options.getOptions(QueryOptions.class);
 
     try {
@@ -78,15 +80,15 @@
           options.getOptions(PackageCacheOptions.class),
           runtime.getDefaultsPackageContent());
     } catch (InterruptedException e) {
-      runtime.getReporter().handle(Event.error("query interrupted"));
+      env.getReporter().handle(Event.error("query interrupted"));
       return ExitCode.INTERRUPTED;
     } catch (AbruptExitException e) {
-      runtime.getReporter().handle(Event.error(null, "Unknown error: " + e.getMessage()));
+      env.getReporter().handle(Event.error(null, "Unknown error: " + e.getMessage()));
       return e.getExitCode();
     }
 
     if (options.getResidue().isEmpty()) {
-      runtime.getReporter().handle(Event.error(String.format(
+      env.getReporter().handle(Event.error(String.format(
           "missing query expression. Type '%s help query' for syntax and help",
           Constants.PRODUCT_NAME)));
       return ExitCode.COMMAND_LINE_ERROR;
@@ -96,7 +98,7 @@
     OutputFormatter formatter =
         OutputFormatter.getFormatter(formatters, queryOptions.outputFormat);
     if (formatter == null) {
-      runtime.getReporter().handle(Event.error(
+      env.getReporter().handle(Event.error(
           String.format("Invalid output format '%s'. Valid values are: %s",
               queryOptions.outputFormat, OutputFormatter.formatterNames(formatters))));
       return ExitCode.COMMAND_LINE_ERROR;
@@ -105,7 +107,7 @@
     String query = Joiner.on(' ').join(options.getResidue());
 
     Set<Setting> settings = queryOptions.toSettings();
-    AbstractBlazeQueryEnvironment<Target> env = newQueryEnvironment(
+    AbstractBlazeQueryEnvironment<Target> queryEnv = newQueryEnvironment(
         runtime,
         queryOptions.keepGoing,
         QueryOutputUtils.orderResults(queryOptions, formatter),
@@ -115,9 +117,9 @@
     // 1. Parse query:
     QueryExpression expr;
     try {
-      expr = QueryExpression.parse(query, env);
+      expr = QueryExpression.parse(query, queryEnv);
     } catch (QueryException e) {
-      runtime.getReporter().handle(Event.error(
+      env.getReporter().handle(Event.error(
           null, "Error while parsing '" + query + "': " + e.getMessage()));
       return ExitCode.COMMAND_LINE_ERROR;
     }
@@ -125,35 +127,34 @@
     // 2. Evaluate expression:
     QueryEvalResult<Target> result;
     try {
-      result = env.evaluateQuery(expr);
+      result = queryEnv.evaluateQuery(expr);
     } catch (QueryException | InterruptedException e) {
       // Keep consistent with reportBuildFileError()
-      runtime
-          .getReporter()
+      env.getReporter()
           // TODO(bazel-team): this is a kludge to fix a bug observed in the wild. We should make
           // sure no null error messages ever get in.
           .handle(Event.error(e.getMessage() == null ? e.toString() : e.getMessage()));
       return ExitCode.ANALYSIS_FAILURE;
     }
 
-    runtime.getReporter().switchToAnsiAllowingHandler();
+    env.getReporter().switchToAnsiAllowingHandler();
     // 3. Output results:
-    PrintStream output = new PrintStream(runtime.getReporter().getOutErr().getOutputStream());
+    PrintStream output = new PrintStream(env.getReporter().getOutErr().getOutputStream());
     try {
       QueryOutputUtils.output(queryOptions, result, formatter, output,
           queryOptions.aspectDeps.createResolver(
-              runtime.getPackageManager(), runtime.getReporter()));
+              runtime.getPackageManager(), env.getReporter()));
     } catch (ClosedByInterruptException | InterruptedException e) {
-      runtime.getReporter().handle(Event.error("query interrupted"));
+      env.getReporter().handle(Event.error("query interrupted"));
       return ExitCode.INTERRUPTED;
     } catch (IOException e) {
-      runtime.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
+      env.getReporter().handle(Event.error("I/O error: " + e.getMessage()));
       return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
     } finally {
       output.flush();
     }
     if (result.getResultSet().isEmpty()) {
-      runtime.getReporter().handle(Event.info("Empty results"));
+      env.getReporter().handle(Event.info("Empty results"));
     }
 
     return result.getSuccess() ? ExitCode.SUCCESS : ExitCode.PARTIAL_ANALYSIS_FAILURE;
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
index 16ed00a..5efe25e 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.buildtool.BuildRequest.BuildRequestOptions;
 import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.buildtool.BuildTool;
 import com.google.devtools.build.lib.buildtool.OutputDirectoryLinksUtils;
 import com.google.devtools.build.lib.buildtool.TargetValidator;
 import com.google.devtools.build.lib.events.Event;
@@ -42,6 +43,7 @@
 import com.google.devtools.build.lib.runtime.BlazeCommand;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.shell.AbnormalTerminationException;
 import com.google.devtools.build.lib.shell.BadExitStatusException;
 import com.google.devtools.build.lib.shell.CommandException;
@@ -109,35 +111,36 @@
   private static final FileType RUNFILES_MANIFEST = FileType.of(".runfiles_manifest");
 
   @VisibleForTesting  // productionVisibility = Visibility.PRIVATE
-  protected BuildResult processRequest(final BlazeRuntime runtime, BuildRequest request) {
-    return runtime.getBuildTool().processRequest(request, new TargetValidator() {
+  protected BuildResult processRequest(final CommandEnvironment env, BuildRequest request) {
+    return new BuildTool(env).processRequest(request, new TargetValidator() {
       @Override
       public void validateTargets(Collection<Target> targets, boolean keepGoing)
           throws LoadingFailedException {
-        RunCommand.this.validateTargets(runtime.getReporter(), targets, keepGoing);
+        RunCommand.this.validateTargets(env.getReporter(), targets, keepGoing);
       }
     });
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) { }
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) { }
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
+    BlazeRuntime runtime = env.getRuntime();
     RunOptions runOptions = options.getOptions(RunOptions.class);
     // This list should look like: ["//executable:target", "arg1", "arg2"]
     List<String> targetAndArgs = options.getResidue();
 
     // The user must at the least specify an executable target.
     if (targetAndArgs.isEmpty()) {
-      runtime.getReporter().handle(Event.error("Must specify a target to run"));
+      env.getReporter().handle(Event.error("Must specify a target to run"));
       return ExitCode.COMMAND_LINE_ERROR;
     }
     String targetString = targetAndArgs.get(0);
     List<String> runTargetArgs = targetAndArgs.subList(1, targetAndArgs.size());
     RunUnder runUnder = options.getOptions(BuildConfiguration.Options.class).runUnder;
 
-    OutErr outErr = runtime.getReporter().getOutErr();
+    OutErr outErr = env.getReporter().getOutErr();
     List<String> targets = (runUnder != null) && (runUnder.getLabel() != null)
         ? ImmutableList.of(targetString, runUnder.getLabel().toString())
         : ImmutableList.of(targetString);
@@ -149,13 +152,13 @@
     currentRunUnder = runUnder;
     BuildResult result;
     try {
-      result = processRequest(runtime, request);
+      result = processRequest(env, request);
     } finally {
       currentRunUnder = null;
     }
 
     if (!result.getSuccess()) {
-      runtime.getReporter().handle(Event.error("Build failed. Not running target"));
+      env.getReporter().handle(Event.error("Build failed. Not running target"));
       return result.getExitCondition();
     }
 
@@ -170,7 +173,7 @@
     if (targetsBuilt != null) {
       int maxTargets = runUnder != null && runUnder.getLabel() != null ? 2 : 1;
       if (targetsBuilt.size() > maxTargets) {
-        runtime.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE));
+        env.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE));
         return ExitCode.COMMAND_LINE_ERROR;
       }
       for (ConfiguredTarget target : targetsBuilt) {
@@ -180,7 +183,7 @@
         }
         if (runUnder != null && target.getLabel().equals(runUnder.getLabel())) {
           if (runUnderTarget != null) {
-            runtime.getReporter().handle(Event.error(
+            env.getReporter().handle(Event.error(
                 null, "Can't identify the run_under target from multiple options?"));
             return ExitCode.COMMAND_LINE_ERROR;
           }
@@ -188,7 +191,7 @@
         } else if (targetToRun == null) {
           targetToRun = target;
         } else {
-          runtime.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE));
+          env.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE));
           return ExitCode.COMMAND_LINE_ERROR;
         }
       }
@@ -198,7 +201,7 @@
       targetToRun = runUnderTarget;
     }
     if (targetToRun == null) {
-      runtime.getReporter().handle(Event.error(NO_TARGET_MESSAGE));
+      env.getReporter().handle(Event.error(NO_TARGET_MESSAGE));
       return ExitCode.COMMAND_LINE_ERROR;
     }
 
@@ -208,14 +211,14 @@
     if (configuration == null) {
       // The target may be an input file, which doesn't have a configuration. In that case, we
       // choose any target configuration.
-      configuration = runtime.getBuildTool().getView().getConfigurationCollection()
+      configuration = runtime.getView().getConfigurationCollection()
           .getTargetConfigurations().get(0);
     }
     Path workingDir;
     try {
       workingDir = ensureRunfilesBuilt(runtime, targetToRun);
     } catch (CommandException e) {
-      runtime.getReporter().handle(Event.error("Error creating runfiles: " + e.getMessage()));
+      env.getReporter().handle(Event.error("Error creating runfiles: " + e.getMessage()));
       return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
     }
 
@@ -298,7 +301,7 @@
       }
     }
 
-    runtime.getReporter().handle(Event.info(
+    env.getReporter().handle(Event.info(
         null, "Running command line: " + ShellEscaper.escapeJoinAll(prettyCmdLine)));
 
     com.google.devtools.build.lib.shell.Command command = new CommandBuilder()
@@ -307,7 +310,7 @@
     try {
       // Restore a raw EventHandler if it is registered. This allows for blaze run to produce the
       // actual output of the command being run even if --color=no is specified.
-      runtime.getReporter().switchToAnsiAllowingHandler();
+      env.getReporter().switchToAnsiAllowingHandler();
 
       // The command API is a little strange in that the following statement
       // will return normally only if the program exits with exit code 0.
@@ -322,13 +325,13 @@
       String message = "Non-zero return code '"
                        + e.getResult().getTerminationStatus().getExitCode()
                        + "' from command: " + e.getMessage();
-      runtime.getReporter().handle(Event.error(message));
+      env.getReporter().handle(Event.error(message));
       return ExitCode.RUN_FAILURE;
     } catch (AbnormalTerminationException e) {
       // The process was likely terminated by a signal in this case.
       return ExitCode.INTERRUPTED;
     } catch (CommandException e) {
-      runtime.getReporter().handle(Event.error("Error running program: " + e.getMessage()));
+      env.getReporter().handle(Event.error("Error running program: " + e.getMessage()));
       return ExitCode.RUN_FAILURE;
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ShutdownCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ShutdownCommand.java
index fe606e6..4e63e7e 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/ShutdownCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ShutdownCommand.java
@@ -15,8 +15,8 @@
 
 import com.google.devtools.build.lib.runtime.BlazeCommand;
 import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.ShutdownBlazeServerException;
-import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionsBase;
@@ -47,12 +47,11 @@
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options)
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options)
       throws ShutdownBlazeServerException {
-
     int limit = options.getOptions(Options.class).heapSizeLimit;
 
     // Iff limit is non-zero, shut down the server if total memory exceeds the
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/TestCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/TestCommand.java
index 962e0c0..9d9ca9d 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/TestCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/TestCommand.java
@@ -18,6 +18,7 @@
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.buildtool.BuildTool;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.exec.ExecutionOptions;
 import com.google.devtools.build.lib.rules.test.TestStrategy;
@@ -27,6 +28,7 @@
 import com.google.devtools.build.lib.runtime.BlazeCommandEventHandler;
 import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.runtime.TerminalTestResultNotifier;
 import com.google.devtools.build.lib.runtime.TerminalTestResultNotifier.TestSummaryOptions;
 import com.google.devtools.build.lib.runtime.TestResultAnalyzer;
@@ -63,15 +65,15 @@
   }
 
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser)
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser)
       throws AbruptExitException {
-    ProjectFileSupport.handleProjectFiles(runtime, optionsParser, commandName());
+    ProjectFileSupport.handleProjectFiles(env, optionsParser, commandName());
 
     TestOutputFormat testOutput = optionsParser.getOptions(ExecutionOptions.class).testOutput;
 
     try {
       if (testOutput == TestStrategy.TestOutputFormat.STREAMED) {
-        runtime.getReporter().handle(Event.warn(
+        env.getReporter().handle(Event.warn(
             "Streamed test output requested so all tests will be run locally, without sharding, " +
              "one at a time"));
         optionsParser.parse(OptionPriority.SOFTWARE_REQUIREMENT,
@@ -84,36 +86,37 @@
   }
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
     TestResultAnalyzer resultAnalyzer = new TestResultAnalyzer(
-        runtime.getExecRoot(),
+        env.getDirectories().getExecRoot(),
         options.getOptions(TestSummaryOptions.class),
         options.getOptions(ExecutionOptions.class),
-        runtime.getEventBus());
+        env.getEventBus());
 
-    printer = new AnsiTerminalPrinter(runtime.getReporter().getOutErr().getOutputStream(),
+    printer = new AnsiTerminalPrinter(env.getReporter().getOutErr().getOutputStream(),
         options.getOptions(BlazeCommandEventHandler.Options.class).useColor());
 
     // Initialize test handler.
     AggregatingTestListener testListener = new AggregatingTestListener(
-        resultAnalyzer, runtime.getEventBus(), runtime.getReporter());
+        resultAnalyzer, env.getEventBus(), env.getReporter());
 
-    runtime.getEventBus().register(testListener);
-    return doTest(runtime, options, testListener);
+    env.getEventBus().register(testListener);
+    return doTest(env, options, testListener);
   }
 
-  private ExitCode doTest(BlazeRuntime runtime,
+  private ExitCode doTest(CommandEnvironment env,
       OptionsProvider options,
       AggregatingTestListener testListener) {
+    BlazeRuntime runtime = env.getRuntime();
     // Run simultaneous build and test.
     List<String> targets = ProjectFileSupport.getTargets(runtime, options);
     BuildRequest request = BuildRequest.create(
         getClass().getAnnotation(Command.class).name(), options,
         runtime.getStartupOptionsProvider(), targets,
-        runtime.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
+        env.getReporter().getOutErr(), runtime.getCommandId(), runtime.getCommandStartTime());
     request.setRunTests();
 
-    BuildResult buildResult = runtime.getBuildTool().processRequest(request, null);
+    BuildResult buildResult = new BuildTool(env).processRequest(request, null);
 
     Collection<ConfiguredTarget> testTargets = buildResult.getTestTargets();
     // TODO(bazel-team): don't handle isEmpty here or fix up a bunch of tests
@@ -121,13 +124,13 @@
       // This can happen if there were errors in the target parsing or loading phase
       // (original exitcode=BUILD_FAILURE) or if there weren't but --noanalyze was given
       // (original exitcode=SUCCESS).
-      runtime.getReporter().handle(Event.error("Couldn't start the build. Unable to run tests"));
+      env.getReporter().handle(Event.error("Couldn't start the build. Unable to run tests"));
       return buildResult.getSuccess() ? ExitCode.PARSING_FAILURE : buildResult.getExitCondition();
     }
     // TODO(bazel-team): the check above shadows NO_TESTS_FOUND, but switching the conditions breaks
     // more tests
     if (testTargets.isEmpty()) {
-      runtime.getReporter().handle(Event.error(
+      env.getReporter().handle(Event.error(
           null, "No test targets were found, yet testing was requested"));
       return buildResult.getSuccess() ? ExitCode.NO_TESTS_FOUND : buildResult.getExitCondition();
     }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/VersionCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/VersionCommand.java
index 2d7b9ae..9ac1bb3 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/VersionCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/VersionCommand.java
@@ -16,8 +16,8 @@
 import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.runtime.BlazeCommand;
-import com.google.devtools.build.lib.runtime.BlazeRuntime;
 import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.common.options.OptionsParser;
 import com.google.devtools.common.options.OptionsProvider;
@@ -34,16 +34,16 @@
          shortDescription = "Prints version information for %{product}.")
 public final class VersionCommand implements BlazeCommand {
   @Override
-  public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) {}
+  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}
 
   @Override
-  public ExitCode exec(BlazeRuntime runtime, OptionsProvider options) {
+  public ExitCode exec(CommandEnvironment env, OptionsProvider options) {
     BlazeVersionInfo info = BlazeVersionInfo.instance();
     if (info.getSummary() == null) {
-      runtime.getReporter().handle(Event.error("Version information not available"));
+      env.getReporter().handle(Event.error("Version information not available"));
       return ExitCode.COMMAND_LINE_ERROR;
     }
-    runtime.getReporter().getOutErr().printOutLn(info.getSummary());
+    env.getReporter().getOutErr().printOutLn(info.getSummary());
     return ExitCode.SUCCESS;
   }
 }