Symlink creation: simplify interfaces

- throw a higher-level exception so callers don't need to
- remove the enableRunfiles parameter from SymlinkTreeActionContext
- move manifest copying into a separate method

I'm considering adding an in-process symlink creation implementation, which is easier after this change.

PiperOrigin-RevId: 281280393
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java
index db515ae..04ff823 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeAction.java
@@ -123,6 +123,10 @@
     return filesetTree;
   }
 
+  public boolean enableRunfiles() {
+    return enableRunfiles;
+  }
+
   @Override
   public String getMnemonic() {
     return "SymlinkTree";
@@ -159,7 +163,7 @@
       throws ActionExecutionException, InterruptedException {
     actionExecutionContext
         .getContext(SymlinkTreeActionContext.class)
-        .createSymlinks(this, actionExecutionContext, enableRunfiles);
+        .createSymlinks(this, actionExecutionContext);
     return ActionResult.EMPTY;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeActionContext.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeActionContext.java
index e7e3280..21f43ec 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeActionContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkTreeActionContext.java
@@ -22,12 +22,7 @@
  */
 public interface SymlinkTreeActionContext extends ActionContext {
 
-  /**
-   * Creates the symlink tree.
-   */
-  void createSymlinks(
-      SymlinkTreeAction action,
-      ActionExecutionContext actionExecutionContext,
-      boolean enableRunfiles)
+  /** Creates the symlink tree. */
+  void createSymlinks(SymlinkTreeAction action, ActionExecutionContext actionExecutionContext)
       throws ActionExecutionException, InterruptedException;
 }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
index 37d6f2b..d96d408 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeHelper.java
@@ -16,8 +16,8 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
-import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.util.CommandBuilder;
@@ -61,27 +61,6 @@
   }
 
   /**
-   * Creates a symlink tree using a CommandBuilder. This means that the symlink tree will always be
-   * present on the developer's workstation. Useful when running commands locally.
-   *
-   * <p>Warning: this method REALLY executes the command on the box Bazel is running on, without any
-   * kind of synchronization, locking, or anything else.
-   *
-   * @param config the configuration that is used for creating the symlink tree.
-   * @throws CommandException
-   */
-  public void createSymlinksUsingCommand(
-      Path execRoot, BinTools binTools, Map<String, String> shellEnvironment, OutErr outErr)
-      throws CommandException {
-    Command command = createCommand(execRoot, binTools, shellEnvironment);
-    if (outErr != null) {
-      command.execute(outErr.getOutputStream(), outErr.getErrorStream());
-    } else {
-      command.execute();
-    }
-  }
-
-  /**
    * Creates symlink tree and output manifest using the {@code build-runfiles.cc} tool.
    *
    * @param enableRunfiles If {@code false} only the output manifest is created.
@@ -94,19 +73,42 @@
       boolean enableRunfiles)
       throws ExecException {
     if (enableRunfiles) {
-      try {
-        createSymlinksUsingCommand(execRoot, binTools, shellEnvironment, outErr);
-      } catch (CommandException e) {
-        throw new UserExecException(CommandUtils.describeCommandFailure(true, e), e);
-      }
+      createSymlinksUsingCommand(execRoot, binTools, shellEnvironment, outErr);
     } else {
-      // Pretend we created the runfiles tree by copying the manifest
-      try {
-        symlinkTreeRoot.createDirectoryAndParents();
-        FileSystemUtils.copyFile(inputManifest, symlinkTreeRoot.getChild("MANIFEST"));
-      } catch (IOException e) {
-        throw new UserExecException(e.getMessage(), e);
+      copyManifest();
+    }
+  }
+
+  /** Copies the input manifest to the output manifest. */
+  public void copyManifest() throws ExecException {
+    // Pretend we created the runfiles tree by copying the manifest
+    try {
+      symlinkTreeRoot.createDirectoryAndParents();
+      FileSystemUtils.copyFile(inputManifest, symlinkTreeRoot.getChild("MANIFEST"));
+    } catch (IOException e) {
+      throw new EnvironmentalExecException(e);
+    }
+  }
+
+  /**
+   * Creates a symlink tree using a CommandBuilder. This means that the symlink tree will always be
+   * present on the developer's workstation. Useful when running commands locally.
+   *
+   * <p>Warning: this method REALLY executes the command on the box Bazel is running on, without any
+   * kind of synchronization, locking, or anything else.
+   */
+  public void createSymlinksUsingCommand(
+      Path execRoot, BinTools binTools, Map<String, String> shellEnvironment, OutErr outErr)
+      throws EnvironmentalExecException {
+    Command command = createCommand(execRoot, binTools, shellEnvironment);
+    try {
+      if (outErr != null) {
+        command.execute(outErr.getOutputStream(), outErr.getErrorStream());
+      } else {
+        command.execute();
       }
+    } catch (CommandException e) {
+      throw new EnvironmentalExecException(CommandUtils.describeCommandFailure(true, e), e);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
index 5ed9764..bd1c7a8 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategy.java
@@ -44,9 +44,7 @@
 
   @Override
   public void createSymlinks(
-      SymlinkTreeAction action,
-      ActionExecutionContext actionExecutionContext,
-      boolean enableRunfiles)
+      SymlinkTreeAction action, ActionExecutionContext actionExecutionContext)
       throws ActionExecutionException, InterruptedException {
     actionExecutionContext.getEventHandler().post(new RunningActionEvent(action, "local"));
     try (AutoProfiler p =
@@ -59,22 +57,17 @@
               actionExecutionContext.getInputPath(action.getOutputManifest()),
               action.isFilesetTree(),
               action.getOutputManifest().getExecPath().getParentDirectory());
+        } else if (!action.enableRunfiles()) {
+          createSymlinkTreeHelper(action, actionExecutionContext).copyManifest();
         } else {
           Map<String, String> resolvedEnv = new LinkedHashMap<>();
           action.getEnvironment().resolve(resolvedEnv, actionExecutionContext.getClientEnv());
-          SymlinkTreeHelper helper =
-              new SymlinkTreeHelper(
-                  actionExecutionContext.getInputPath(action.getInputManifest()),
-                  actionExecutionContext
-                      .getInputPath(action.getOutputManifest())
-                      .getParentDirectory(),
-                  action.isFilesetTree());
-          helper.createSymlinks(
-              actionExecutionContext.getExecRoot(),
-              actionExecutionContext.getFileOutErr(),
-              binTools,
-              resolvedEnv,
-              enableRunfiles);
+          createSymlinkTreeHelper(action, actionExecutionContext)
+              .createSymlinksUsingCommand(
+                  actionExecutionContext.getExecRoot(),
+                  binTools,
+                  resolvedEnv,
+                  actionExecutionContext.getFileOutErr());
         }
       } catch (ExecException e) {
         throw e.toActionExecutionException(
@@ -82,4 +75,12 @@
       }
     }
   }
+
+  private static SymlinkTreeHelper createSymlinkTreeHelper(
+      SymlinkTreeAction action, ActionExecutionContext actionExecutionContext) {
+    return new SymlinkTreeHelper(
+        actionExecutionContext.getInputPath(action.getInputManifest()),
+        actionExecutionContext.getInputPath(action.getOutputManifest()).getParentDirectory(),
+        action.isFilesetTree());
+  }
 }
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 3a89854..1256a20 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
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.CommandLine;
 import com.google.devtools.build.lib.actions.CommandLineExpansionException;
+import com.google.devtools.build.lib.actions.EnvironmentalExecException;
 import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.FilesToRunProvider;
@@ -66,7 +67,6 @@
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.server.CommandProtos.EnvironmentVariable;
 import com.google.devtools.build.lib.server.CommandProtos.ExecRequest;
-import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.shell.ShellUtils;
 import com.google.devtools.build.lib.util.CommandDescriptionForm;
 import com.google.devtools.build.lib.util.CommandFailureUtils;
@@ -389,7 +389,7 @@
         runfilesDir = ensureRunfilesBuilt(env, runfilesSupport,
             env.getSkyframeExecutor().getConfiguration(env.getReporter(),
                 targetToRun.getConfigurationKey()));
-      } catch (CommandException e) {
+      } catch (EnvironmentalExecException e) {
         env.getReporter().handle(Event.error("Error creating runfiles: " + e.getMessage()));
         return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
       }
@@ -567,11 +567,12 @@
   }
 
   /**
-   * Ensures that runfiles are built for the specified target. If they already
-   * are, does nothing, otherwise builds them.
+   * Ensures that runfiles are built for the specified target. If they already are, does nothing,
+   * otherwise builds them.
    */
-  private Path ensureRunfilesBuilt(CommandEnvironment env, RunfilesSupport runfilesSupport,
-      BuildConfiguration configuration) throws CommandException {
+  private static Path ensureRunfilesBuilt(
+      CommandEnvironment env, RunfilesSupport runfilesSupport, BuildConfiguration configuration)
+      throws EnvironmentalExecException {
     Artifact manifest = Preconditions.checkNotNull(runfilesSupport.getRunfilesManifest());
     PathFragment runfilesDir = runfilesSupport.getRunfilesDirectoryExecPath();
     Path workingDir = env.getExecRoot().getRelative(runfilesDir);