For Skymeld, eagerly plant the symlinks from execroot to the source root.

This enables Skymeld to run local actions for builds with a single package_path.

PiperOrigin-RevId: 440334907
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
index 7790816..c5aaf15 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -205,7 +205,11 @@
           Set<AspectKey> builtAspects = new HashSet<>();
 
           try (SilentCloseable c = Profiler.instance().profile("ExecutionTool.init")) {
-            executionTool.prepareForExecution(request.getId(), builtTargets, builtAspects);
+            executionTool.prepareForExecution(
+                request.getId(),
+                builtTargets,
+                builtAspects,
+                loadingResult.getNotSymlinkedInExecrootDirectories());
           }
 
           // TODO(b/199053098): implement support for --nobuild.
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
index 5f2969b..1283001 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
@@ -247,7 +247,10 @@
    * tests for these setup steps.
    */
   public void prepareForExecution(
-      UUID buildId, Set<ConfiguredTargetKey> builtTargets, Set<AspectKey> builtAspects)
+      UUID buildId,
+      Set<ConfiguredTargetKey> builtTargets,
+      Set<AspectKey> builtAspects,
+      ImmutableSortedSet<String> notSymlinkedInExecrootDirectories)
       throws AbruptExitException, BuildFailedException, InterruptedException {
     init();
     BuildRequestOptions options = request.getBuildOptions();
@@ -260,12 +263,33 @@
         "--experimental_merged_skyframe_analysis_execution requires a single package path entry."
             + " Found a list of size: %s",
         pkgPathEntries.size());
-    Root singleSourceRoot = Iterables.getOnlyElement(pkgPathEntries);
-    PackageRoots noSymlinkPackageRoots = new PackageRootsNoSymlinkCreation(singleSourceRoot);
-    env.getEventBus().post(new ExecRootPreparedEvent(noSymlinkPackageRoots.getPackageRootsMap()));
-    env.getSkyframeBuildView()
-        .getArtifactFactory()
-        .setPackageRoots(noSymlinkPackageRoots.getPackageRootLookup());
+
+    try (SilentCloseable c = Profiler.instance().profile("preparingExecroot")) {
+      Root singleSourceRoot = Iterables.getOnlyElement(pkgPathEntries);
+      PackageRoots noSymlinkPackageRoots = new PackageRootsNoSymlinkCreation(singleSourceRoot);
+      try {
+        SymlinkForest.eagerlyPlantSymlinkForestSinglePackagePath(
+            getExecRoot(),
+            singleSourceRoot.asPath(),
+            /*prefix=*/ env.getDirectories().getProductName() + "-",
+            notSymlinkedInExecrootDirectories,
+            request.getOptions(BuildLanguageOptions.class).experimentalSiblingRepositoryLayout);
+      } catch (IOException e) {
+        throw new AbruptExitException(
+            DetailedExitCode.of(
+                FailureDetail.newBuilder()
+                    .setMessage("Failed to prepare the symlink forest")
+                    .setSymlinkForest(
+                        FailureDetails.SymlinkForest.newBuilder()
+                            .setCode(FailureDetails.SymlinkForest.Code.CREATION_FAILED))
+                    .build()),
+            e);
+      }
+      env.getEventBus().post(new ExecRootPreparedEvent(noSymlinkPackageRoots.getPackageRootsMap()));
+      env.getSkyframeBuildView()
+          .getArtifactFactory()
+          .setPackageRoots(noSymlinkPackageRoots.getPackageRootLookup());
+    }
 
     OutputService outputService = env.getOutputService();
     ModifiedFileSet modifiedOutputFiles = ModifiedFileSet.EVERYTHING_MODIFIED;
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java b/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
index 9a70756..5b948dd 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
@@ -112,7 +112,9 @@
    */
   @VisibleForTesting
   @ThreadSafety.ThreadSafe
-  void deleteTreesBelowNotPrefixed(Path dir, String prefix) throws IOException {
+  static void deleteTreesBelowNotPrefixed(
+      Path dir, String prefix, ImmutableSortedSet<String> notSymlinkedInExecrootDirectories)
+      throws IOException {
 
     for (Path p : dir.getDirectoryEntries()) {
 
@@ -337,7 +339,7 @@
    * @return the symlinks that have been planted
    */
   public ImmutableList<Path> plantSymlinkForest() throws IOException, AbruptExitException {
-    deleteTreesBelowNotPrefixed(execroot, prefix);
+    deleteTreesBelowNotPrefixed(execroot, prefix, notSymlinkedInExecrootDirectories);
 
     if (siblingRepositoryLayout) {
       // Delete execroot/../<symlinks> to directories representing external repositories.
@@ -419,6 +421,38 @@
     return plantedSymlinks.build();
   }
 
+  /**
+   * Eagerly plant the symlinks from execroot to the source root provided by the single package path
+   * of the current build. Only works with a single package path. Before planting the new symlinks,
+   * remove all existing symlinks in execroot which don't match certain criteria.
+   */
+  public static void eagerlyPlantSymlinkForestSinglePackagePath(
+      Path execroot,
+      Path sourceRoot,
+      String prefix,
+      ImmutableSortedSet<String> notSymlinkedInExecrootDirectories,
+      boolean siblingRepositoryLayout)
+      throws IOException {
+    deleteTreesBelowNotPrefixed(execroot, prefix, notSymlinkedInExecrootDirectories);
+
+    // Plant everything under the single source root.
+    for (Path target : sourceRoot.getDirectoryEntries()) {
+
+      String baseName = target.getBaseName();
+      if (notSymlinkedInExecrootDirectories.contains(baseName)) {
+        continue;
+      }
+      Path execPath = execroot.getRelative(baseName);
+      // Create any links that don't start with bazel-, and ignore external/ directory if
+      // user has it in the source tree because it conflicts with external repository location.
+      if (!baseName.startsWith(prefix)
+          && (siblingRepositoryLayout
+              || !baseName.equals(LabelConstants.EXTERNAL_PATH_PREFIX.getBaseName()))) {
+        execPath.createSymbolicLink(target);
+      }
+    }
+  }
+
   private static DetailedExitCode detailedSymlinkForestExitCode(String message, Code code) {
     return DetailedExitCode.of(
         FailureDetail.newBuilder()
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
index 706a54c..b3028ad 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
@@ -640,8 +640,7 @@
         "//src/main/java/com/google/devtools/build/lib:runtime",
         "//src/main/java/com/google/devtools/build/lib/actions",
         "//src/main/java/com/google/devtools/build/lib/analysis:view_creation_failed_exception",
-        "//src/main/java/com/google/devtools/build/lib/util/io",
-        "//src/main/protobuf:failure_details_java_proto",
+        "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/test/java/com/google/devtools/build/lib/buildtool/util",
         "//third_party:guava",
         "//third_party:junit4",
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/SkymeldBuildIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/buildtool/SkymeldBuildIntegrationTest.java
index bc2a0be..406b46a 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/SkymeldBuildIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/SkymeldBuildIntegrationTest.java
@@ -14,14 +14,13 @@
 package com.google.devtools.build.lib.buildtool;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.devtools.build.lib.server.FailureDetails.Spawn.Code.NON_ZERO_EXIT;
 import static org.junit.Assert.assertThrows;
 
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.actions.BuildFailedException;
 import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
 import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
-import com.google.devtools.build.lib.util.io.RecordingOutErr;
+import com.google.devtools.build.lib.vfs.Path;
 import com.google.testing.junit.testparameterinjector.TestParameter;
 import com.google.testing.junit.testparameterinjector.TestParameterInjector;
 import java.io.IOException;
@@ -153,7 +152,7 @@
   }
 
   @Test
-  public void noSymlinkPlantedLocalAction_failureNoSuchFileOrDirectory() throws Exception {
+  public void symlinkPlantedLocalAction_success() throws Exception {
     addOptions("--spawn_strategy=standalone");
     write(
         "foo/BUILD",
@@ -161,21 +160,64 @@
         "  name = 'foo',",
         "  srcs = ['foo.in'],",
         "  outs = ['foo.out'],",
-        "  cmd = 'cp foo.in $(location foo.out)'",
+        "  cmd = 'cp $< $@'",
         ")");
     write("foo/foo.in");
 
-    outErr = new RecordingOutErr();
-    BuildFailedException e =
-        assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo"));
-    String err = ((RecordingOutErr) outErr).errAsLatin1();
+    BuildResult result = buildTarget("//foo:foo");
 
-    assertThat(e.getDetailedExitCode().getFailureDetail().getSpawn().getCode())
-        .isEqualTo(NON_ZERO_EXIT);
-    assertThat(err)
-        .contains(
-            "Executing genrule //foo:foo failed: (Exit 1): bash failed: error executing command"
-                + " (from target //foo:foo)");
-    assertThat(err).contains("No such file or directory");
+    assertThat(result.getSuccess()).isTrue();
+    assertSingleOutputBuilt("//foo:foo");
+  }
+
+  @Test
+  public void symlinksPlanted() throws Exception {
+    Path execroot = directories.getExecRoot(directories.getWorkspace().getBaseName());
+    writeMyRuleBzl();
+    Path fooDir =
+        write(
+                "foo/BUILD",
+                "load('//foo:my_rule.bzl', 'my_rule')",
+                "my_rule(name = 'foo', srcs = ['foo.in'])")
+            .getParentDirectory();
+    write("foo/foo.in");
+    Path unusedDir = write("unused/dummy").getParentDirectory();
+
+    // Before the build: no symlink.
+    assertThat(execroot.getRelative("foo").exists()).isFalse();
+
+    buildTarget("//foo:foo");
+
+    // After the build: symlinks to the source directory, even unused packages.
+    assertThat(execroot.getRelative("foo").resolveSymbolicLinks()).isEqualTo(fooDir);
+    assertThat(execroot.getRelative("unused").resolveSymbolicLinks()).isEqualTo(unusedDir);
+  }
+
+  @Test
+  public void symlinksReplantedEachBuild() throws Exception {
+    Path execroot = directories.getExecRoot(directories.getWorkspace().getBaseName());
+    writeMyRuleBzl();
+    Path fooDir =
+        write(
+                "foo/BUILD",
+                "load('//foo:my_rule.bzl', 'my_rule')",
+                "my_rule(name = 'foo', srcs = ['foo.in'])")
+            .getParentDirectory();
+    write("foo/foo.in");
+    Path unusedDir = write("unused/dummy").getParentDirectory();
+
+    buildTarget("//foo:foo");
+
+    // After the 1st build: symlinks to the source directory, even unused packages.
+    assertThat(execroot.getRelative("foo").resolveSymbolicLinks()).isEqualTo(fooDir);
+    assertThat(execroot.getRelative("unused").resolveSymbolicLinks()).isEqualTo(unusedDir);
+
+    unusedDir.deleteTree();
+
+    buildTarget("//foo:foo");
+
+    // After the 2nd build: symlink to unusedDir is gone, since the package itself was deleted.
+    assertThat(execroot.getRelative("foo").resolveSymbolicLinks()).isEqualTo(fooDir);
+    assertThat(execroot.getRelative("unused").exists()).isFalse();
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java b/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java
index d42ad82..4df48ae 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java
@@ -138,7 +138,8 @@
   @Test
   public void testDeleteTreesBelowNotPrefixed() throws IOException {
     createTestDirectoryTree();
-    new SymlinkForest(ImmutableMap.of(), topDir, "").deleteTreesBelowNotPrefixed(topDir, "file-");
+    SymlinkForest.deleteTreesBelowNotPrefixed(
+        topDir, "file-", /*notSymlinkedInExecrootDirectories=*/ ImmutableSortedSet.of());
     assertThat(file1.exists()).isTrue();
     assertThat(file2.exists()).isTrue();
     assertThat(aDir.exists()).isFalse();