Strip the execution root from target paths in FilesetOutputSymlink.

RELNOTES: None.
PiperOrigin-RevId: 215754628
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java b/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
index fd89e58..73a9499 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/FilesetOutputSymlink.java
@@ -26,8 +26,16 @@
   public abstract PathFragment getName();
 
   /**
-   * Target of the symlink. This may be relative to the target's location if the target itself is a
-   * relative symlink. We can override it by using FilesetEntry.symlinks = 'dereference'.
+   * Target of the symlink.
+   *
+   * <p>This path is one of the following:
+   *
+   * <ol>
+   *   <li>Relative to the execution root, in which case {@link #isRelativeToExecRoot} will return
+   *       {@code true}.
+   *   <li>An absolute path to the source tree.
+   *   <li>A relative path that should be considered relative to the link.
+   * </ol>
    */
   public abstract PathFragment getTargetPath();
 
@@ -41,8 +49,21 @@
   /** true if the target is a generated artifact */
   public abstract boolean isGeneratedTarget();
 
+  /** Returns {@code true} if this symlink is relative to the execution root. */
+  public abstract boolean isRelativeToExecRoot();
+
+  /**
+   * Reconstitutes the original target path of this symlink.
+   *
+   * <p>This method essentially performs the inverse of what is done in {@link #create}. If the
+   * execution root was stripped originally, it is re-prepended.
+   */
+  public final PathFragment reconstituteTargetPath(PathFragment execRoot) {
+    return isRelativeToExecRoot() ? execRoot.getRelative(getTargetPath()) : getTargetPath();
+  }
+
   @Override
-  public String toString() {
+  public final String toString() {
     if (getMetadata() == STRIPPED_METADATA) {
       return String.format(
           "FilesetOutputSymlink(%s -> %s)",
@@ -55,19 +76,60 @@
   }
 
   @VisibleForTesting
-  public static FilesetOutputSymlink createForTesting(PathFragment name, PathFragment target) {
-    return new AutoValue_FilesetOutputSymlink(name, target, STRIPPED_METADATA, false);
+  public static FilesetOutputSymlink createForTesting(
+      PathFragment name, PathFragment target, PathFragment execRoot) {
+    return create(name, target, STRIPPED_METADATA, false, execRoot);
+  }
+
+  @VisibleForTesting
+  public static FilesetOutputSymlink createAlreadyRelativizedForTesting(
+      PathFragment name, PathFragment target, boolean isRelativeToExecRoot) {
+    return createAlreadyRelativized(name, target, STRIPPED_METADATA, false, isRelativeToExecRoot);
   }
 
   /**
+   * Creates a {@link FilesetOutputSymlink}.
+   *
+   * <p>To facilitate cross-device sharing, {@code target} will have the machine-local {@code
+   * execRoot} stripped if necessary. If this happens, {@link #isRelativeToExecRoot} will return
+   * {@code true}.
+   *
    * @param name relative path under the Fileset's output directory, including FilesetEntry.destdir
    *     with and FilesetEntry.strip_prefix applied (if applicable)
    * @param target relative or absolute value of the link
    * @param metadata metadata corresponding to the target.
    * @param isGeneratedTarget true if the target is generated.
+   * @param execRoot the execution root
    */
   public static FilesetOutputSymlink create(
-      PathFragment name, PathFragment target, Object metadata, boolean isGeneratedTarget) {
-    return new AutoValue_FilesetOutputSymlink(name, target, metadata, isGeneratedTarget);
+      PathFragment name,
+      PathFragment target,
+      Object metadata,
+      boolean isGeneratedTarget,
+      PathFragment execRoot) {
+    boolean isRelativeToExecRoot = false;
+    // Check if the target is under the execution root. This is not always the case because the
+    // target may point to a source artifact or it may point to another symlink, in which case the
+    // target path is already relative.
+    if (target.startsWith(execRoot)) {
+      target = target.relativeTo(execRoot);
+      isRelativeToExecRoot = true;
+    }
+    return createAlreadyRelativized(
+        name, target, metadata, isGeneratedTarget, isRelativeToExecRoot);
+  }
+
+  /**
+   * Same as {@link #create}, except assumes that {@code target} already had the execution root
+   * stripped if necessary.
+   */
+  public static FilesetOutputSymlink createAlreadyRelativized(
+      PathFragment name,
+      PathFragment target,
+      Object metadata,
+      boolean isGeneratedTarget,
+      boolean isRelativeToExecRoot) {
+    return new AutoValue_FilesetOutputSymlink(
+        name, target, metadata, isGeneratedTarget, isRelativeToExecRoot);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkCustomCommandLine.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkCustomCommandLine.java
index 133aaf3..53bfb09 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkCustomCommandLine.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkCustomCommandLine.java
@@ -22,6 +22,7 @@
 import com.google.devtools.build.lib.actions.ActionKeyContext;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
+import com.google.devtools.build.lib.actions.ArtifactRoot;
 import com.google.devtools.build.lib.actions.CommandLine;
 import com.google.devtools.build.lib.actions.CommandLineExpansionException;
 import com.google.devtools.build.lib.actions.CommandLineItem;
@@ -358,7 +359,8 @@
             FilesetManifest.constructFilesetManifest(
                 artifactExpander.getFileset(fileset),
                 fileset.getExecPath(),
-                RelativeSymlinkBehavior.IGNORE);
+                RelativeSymlinkBehavior.IGNORE,
+                extractExecRoot(fileset));
         for (PathFragment relativePath : filesetManifest.getEntries().keySet()) {
           expandedValues.add(new FilesetSymlinkFile(fileset, relativePath));
         }
@@ -367,6 +369,14 @@
       }
     }
 
+    // TODO(b/117267351): Pass in the exec root more cleanly, or put relative paths in the manifest.
+    private static PathFragment extractExecRoot(Artifact fileset) {
+      Preconditions.checkArgument(!fileset.isSourceArtifact(), fileset);
+      ArtifactRoot root = fileset.getRoot();
+      PathFragment rootFrag = root.getRoot().asPath().asFragment();
+      return rootFrag.subFragment(0, rootFrag.segmentCount() - root.getExecPath().segmentCount());
+    }
+
     private int addToFingerprint(
         List<Object> arguments,
         int argi,
diff --git a/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java b/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
index 40e3842..220beec 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
@@ -16,6 +16,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.LineProcessor;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
@@ -75,16 +76,17 @@
   public static FilesetManifest constructFilesetManifest(
       List<FilesetOutputSymlink> outputSymlinks,
       PathFragment targetPrefix,
-      RelativeSymlinkBehavior relSymlinkbehavior)
+      RelativeSymlinkBehavior relSymlinkBehavior,
+      PathFragment execRoot)
       throws IOException {
     LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>();
     Map<PathFragment, String> relativeLinks = new HashMap<>();
     Map<String, FileArtifactValue> artifactValues = new HashMap<>();
     for (FilesetOutputSymlink outputSymlink : outputSymlinks) {
       PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.getName());
-      String artifact = outputSymlink.getTargetPath().getPathString();
-      artifact = artifact.isEmpty() ? null : artifact;
-      addSymlinkEntry(artifact, fullLocation, relSymlinkbehavior, entries, relativeLinks);
+      PathFragment linkTarget = outputSymlink.reconstituteTargetPath(execRoot);
+      String artifact = Strings.emptyToNull(linkTarget.getPathString());
+      addSymlinkEntry(artifact, fullLocation, relSymlinkBehavior, entries, relativeLinks);
       if (outputSymlink.getMetadata() instanceof FileArtifactValue) {
         artifactValues.put(artifact, (FileArtifactValue) outputSymlink.getMetadata());
       }
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java
index 113ff88..25d4c81 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnInputExpander.java
@@ -182,7 +182,7 @@
       ImmutableList<FilesetOutputSymlink> outputSymlinks = filesetMappings.get(fileset);
       FilesetManifest filesetManifest =
           FilesetManifest.constructFilesetManifest(
-              outputSymlinks, fileset.getExecPath(), relSymlinkBehavior);
+              outputSymlinks, fileset.getExecPath(), relSymlinkBehavior, execRoot.asFragment());
 
       for (Map.Entry<PathFragment, String> mapping : filesetManifest.getEntries().entrySet()) {
         String value = mapping.getValue();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
index aa155b1..5312ca9 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
@@ -47,6 +47,12 @@
     }
   }
 
+  private final PathFragment execRoot;
+
+  public FilesetEntryFunction(PathFragment execRoot) {
+    this.execRoot = execRoot;
+  }
+
   @Override
   public SkyValue compute(SkyKey key, Environment env)
       throws FilesetEntryFunctionException, InterruptedException {
@@ -180,7 +186,7 @@
   }
 
   /** Stores an output symlink unless it would overwrite an existing one. */
-  private static void maybeStoreSymlink(
+  private void maybeStoreSymlink(
       PathFragment linkName,
       PathFragment linkTarget,
       Object metadata,
@@ -190,7 +196,8 @@
     linkName = destPath.getRelative(linkName);
     if (!result.containsKey(linkName)) {
       result.put(
-          linkName, FilesetOutputSymlink.create(linkName, linkTarget, metadata, isGenerated));
+          linkName,
+          FilesetOutputSymlink.create(linkName, linkTarget, metadata, isGenerated, execRoot));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index 300cd4e..bf7ae1d 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -552,7 +552,9 @@
     this.actionExecutionFunction = actionExecutionFunction;
     map.put(SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL,
         new RecursiveFilesystemTraversalFunction());
-    map.put(SkyFunctions.FILESET_ENTRY, new FilesetEntryFunction());
+    map.put(
+        SkyFunctions.FILESET_ENTRY,
+        new FilesetEntryFunction(directories.getExecRoot().asFragment()));
     map.put(
         SkyFunctions.ACTION_TEMPLATE_EXPANSION,
         new ActionTemplateExpansionFunction(actionKeyContext));