Symlink creation: add flag for in-process creation

This is in preparation for skipping manifest creation. If we don't have
an input manifest and no output service, we can't fall back to the
subprocess method, which requires a manifest. Instead, we will fall back
to an in-process implementation. This should be rare.

This also provides a fallback mechanism if the embedded tool is
unavailable, and we may switch this on by default if the performance is
acceptable.

PiperOrigin-RevId: 282895566
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java
index b355035..9bb02cc 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/SymlinkTreeStrategyTest.java
@@ -15,9 +15,11 @@
 package com.google.devtools.build.lib.exec;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -33,8 +35,11 @@
 import com.google.devtools.build.lib.analysis.actions.SymlinkTreeActionContext;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.OutputService;
+import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Symlinks;
 import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -66,7 +71,7 @@
     when(outputService.canCreateSymlinkTree()).thenReturn(true);
 
     Artifact inputManifest = getBinArtifactWithNoOwner("dir/manifest.in");
-    Artifact outputManifest = getBinArtifactWithNoOwner("dir/MANIFEST");
+    Artifact outputManifest = getBinArtifactWithNoOwner("dir.runfiles/MANIFEST");
     Artifact runfile = getBinArtifactWithNoOwner("dir/runfile");
     doAnswer(
             (i) -> {
@@ -89,7 +94,8 @@
             outputManifest,
             /*filesetTree=*/ false,
             ActionEnvironment.EMPTY,
-            /*enableRunfiles=*/ true);
+            /*enableRunfiles=*/ true,
+            /*inprocessSymlinkCreation=*/ false);
 
     action.execute(context);
 
@@ -103,4 +109,49 @@
             PathFragment.create("TESTING/dir/empty"),
             null);
   }
+
+  @Test
+  public void inprocessSymlinkCreation() throws Exception {
+    ActionExecutionContext context = mock(ActionExecutionContext.class);
+    OutputService outputService = mock(OutputService.class);
+    StoredEventHandler eventHandler = new StoredEventHandler();
+
+    when(context.getContext(SymlinkTreeActionContext.class))
+        .thenReturn(new SymlinkTreeStrategy(outputService, null));
+    when(context.getInputPath(any())).thenAnswer((i) -> ((Artifact) i.getArgument(0)).getPath());
+    when(context.getEventHandler()).thenReturn(eventHandler);
+    when(outputService.canCreateSymlinkTree()).thenReturn(false);
+
+    Artifact inputManifest = getBinArtifactWithNoOwner("dir/manifest.in");
+    Artifact outputManifest = getBinArtifactWithNoOwner("dir.runfiles/MANIFEST");
+    Artifact runfile = getBinArtifactWithNoOwner("dir/runfile");
+
+    Runfiles runfiles =
+        new Runfiles.Builder("TESTING", false)
+            .setEmptyFilesSupplier((paths) -> ImmutableList.of(PathFragment.create("dir/empty")))
+            .addArtifact(runfile)
+            .build();
+    SymlinkTreeAction action =
+        new SymlinkTreeAction(
+            ActionsTestUtil.NULL_ACTION_OWNER,
+            inputManifest,
+            runfiles,
+            outputManifest,
+            /*filesetTree=*/ false,
+            ActionEnvironment.EMPTY,
+            /*enableRunfiles=*/ true,
+            /*inprocessSymlinkCreation=*/ true);
+
+    action.execute(context);
+    // Check that the OutputService is not used.
+    verify(outputService, never()).createSymlinkTree(any(), any());
+
+    Path p = outputManifest.getPath().getParentDirectory().getRelative("TESTING/dir/runfile");
+    assertWithMessage("Path %s expected to exist", p).that(p.exists(Symlinks.NOFOLLOW)).isTrue();
+    assertWithMessage("Path %s expected to be a symlink", p).that(p.isSymbolicLink()).isTrue();
+    assertThat(p.readSymbolicLink()).isEqualTo(runfile.getPath().asFragment());
+    Path q = outputManifest.getPath().getParentDirectory().getRelative("TESTING/dir/empty");
+    assertWithMessage("Path %s expected to be a file", q).that(q.isFile()).isTrue();
+    assertThat(FileSystemUtils.readContent(q)).isEmpty();
+  }
 }